JavaScript 基础

JavaScript 的编译过程#

  1. 分词/词法分析(Tokenizing/Lexing)
  2. 解析/语法分析(Parsing)

    生成 AST

  3. 代码生成

JavaScript 的作用域#

定义#

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

暂时性死区#

遍历嵌套作用域链的规则#

引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没有找到,查找过程都会停止。

遮蔽效应#

在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)

动态修改(欺骗)此法作用域#

缺点:欺骗词法作用域会导致性能下降

  1. eval
  2. with

块级作用域的替代方案#

可以在 ES6 环境运行的代码

{
let a = 2;
console.log(a); // 2
}
console.log(a); // Reference Error

在 ES6 之前的环境的实现

try {
throw 2;
} catch (a) {
console.log(a); // 2
}
console.log(a); // Reference Error

编程语言作用域的工作模型#

  1. 词法作用域,被大多数编程语言所采用
  2. 动态作用域(Bash 脚本、Perl)

编程语言作用域单元#

  1. 函数作用域
  2. 块级作用域

JavaScript 中创建块级作用域的几种方式#

  1. with
  2. try/cacth
  3. let
  4. const

作用域闭包#

闭包的定义#

  • 广义定义

    当函数可以记住并访问所在的词法作用域时,就产生了闭包。

  • 严格定义

    函数是在当前词法作用域之外执行

function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();

bar()拥有涵盖 foo()内部作用域的闭包,使得该作用域能够一直存活,以供 bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫做闭包。

本质上,无论何时何地,如果将(访问它们各自词法作用域的)函数当做第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务重,只要使用了回调函数,实际上就是在使用闭包!

循环和闭包#

for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, j * 1000);
})(i);
}

闭包的应用-模块模式#

模块模式的两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

  1. 普通模块
function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
}
var foo = CoolModule();
foo.doSomething();
foo.doAnother();
  1. 单例模块
var foo = (function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
})();
foo.doSomething();
foo.doAnother();

关于 this#

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性,会在函数执行的过程中得到。

this 的绑定规则#

  • 默认绑定

  • 隐式绑定

  • 显式绑定

    1. Function.prototype.call
    2. Function.prototype.apply
    3. Function.prototype.bind

    手写 bind

    function myBind(fun, obj) {
    return function () {
    fun.apply(obj, arguments);
    };
    }
  • new 绑定

    在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已

    实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

    使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

    1. 创建(或者说构造)一个全新的对象。
    2. 这个新对象会被执行[[Prototype]]连接。
    3. 这个对象会被绑定到函数调用的 this。
    4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新的对象。
    function myNew() {
    // 拿到要作用的构造函数和参数
    const [constructor, ...args] = [...arguments];
    // 简单处理下箭头函数不可以作为构造函数
    if (constructor.toString().includes('=>')) {
    throw '箭头函数不能作为构造函数';
    }
    //0. 创建一个空对象
    const obj = {};
    //1. 将对象的原型指向构造函数中的prototype属性
    obj.__proto__ = constructor.prototype;
    //2. 绑定this指向
    const res = constructor.apply(obj, args);
    //3. 返回值问题,如果该函数没有返回对象,则返回this
    return res instanceof Object ? res : obj;
    }

this 绑定规则优先级#

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

原型#

原型是对象间的关联关系,这种关联关系可以称之为“委托”

继承#

传统面向对象的继承是复制操作(类复制到对象),原型继承是一种对象关联机制。

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反 JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。“委托”这个术语可以更加准确的描述 JavaScript 中对象的关联机制。

继承的实现#
  • 原型继承
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function () {
return this.name;
};
function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
// ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
// ES6之后可以直接修改现有的Bar.prototype
// Object.setPrototypeOf(Bar.prototype, Foo.prototype);
Bar.prototype.myLabel = function () {
return this.label;
};
var bar = new Bar('a', 'obj a');
bar.myName();
bar.myLabel();
  • 寄生继承
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.inigition = function () {
console.log('Turning on my engine');
};
Vehicle.prototype.drive = function () {
this.ignition();
console.log('Steering and moving forward');
};
// "寄生类"Car
function Car() {
var car = new Vehicle();
car.wheels = 4;
var vehDrive = car.drive;
car.drive = function () {
vehDrive.call(this);
console.log(`Rolling on all ${this.wheels} wheels`);
};
return car;
}
Object.create 的 polifill 代码#
if (!Object.create)
Object.create = function (o) {
function F() {}
Foo.prototype = o;
return new F();
};
}

“类”和委托设计模式#

  • 面向对象风格(面向类设计模式)

    参见上一节“原型继承”

    该设计模式存在思维模式不匹配的问题

  • 对象关联风格(行为委托设计模式)

Foo = {
init: function (who) {
this.me = who;
},
identify: function () {
return 'I am ' + this.me;
},
};
Bar = Object.create(Foo);
Bar.speek = function () {
alert('Hello' + this.identify() + '.');
};
var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');
b1.speek();
b2.speek();

内省#

内省就是检查实例的类型

  • instanceof

    用于检查面向类设计模式创建对象的类型

  • Object.prototype.isPrototypeOf() 和 Object.getPrototypeOf()

    用于检查使用行为委托设计模式创建对象的类型

for...in 和 in 操作符#

使用 for...in 遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到(并且是 enumberable)的属性都会被枚举。使用 in 操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)。

基本类型和复杂类型是储存在哪里?#

  1. 基本类型储存在栈中,但是一旦被闭包引用则成为常住内存,会储存在内存堆中。
  2. 复杂类型会储存在内存堆中

箭头函数相较于普通函数的特殊特性#

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 函数不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  3. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  4. 不可以使用 new 命令,因为:没有自己的 this,无法调用 call,apply。没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 proto

ES 模块和 CommonJS 模块的差异#

引用

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJS 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段。

第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。