1. 作用域
1.1 角色
-
引擎
从头到尾负责整个JavaScript程序的编译及执行过程。
-
编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
-
作用域
作用域引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
1.2 编译过程
-
RHS查询 (赋值操作的左侧)
简单地找到某个变量
-
LHS查询(赋值操作的右侧)
试图找到变量地容器本身,从而可以对其赋值
1.3 作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
1.4 两种异常
-
ReferenceError
RHS查询在所有嵌套的作用域中遍寻不到所需的变量,即未声明
(非严格模式下)当引擎执行LHS查询时,如果在全局作用域中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量
-
TypeError
可以在作用域中找到变量,但是对结果的操作是非法的
1.5 小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
2. 词法作用域
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
欺骗词法
-
eval
JavaScript中的eval(…)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码
function foo(str, a) { eval(str); // 欺骗 console.log(a,b); } var b = 2; foo("val b =3", 1); // 1 3
在严格模式的程序中,eval(…)在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
-
with
with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身
var obj = { a: 1, b: 2, c: 3 }; // 重复obj obj.a = 2; // 快捷方式 with(obj) { a = 3; b = 4; c = 5; }
eval(…)函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域
3. 函数作用域
在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容
4. 块作用域
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息
-
with
-
try/catch
非常少有人会注意到JavaScript的ES3规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效
-
let
et关键字可以将变量绑定到所在的任意作用域中(通常是{ … }内部)。换句话说,let为其声明的变量隐式地劫持了所在的块作用域
使用let进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”
{ console.log(bar); // ReferenceError let bar = 2; }
5. 提升
一个普通块内部的函数声明通常会被提升到所在作用域的顶部
当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;和a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段
- 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
// 相当于
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
- 函数声明会被提升,但是函数表达式却不会被提升
foo(); // TypeError
var foo = function bar() {
}
-
函数首先被提升,然后才是变量
foo(); var foo; function foo() { console.log(1); } foo = function() { console.log(2); } // 相当于 function foo() { console.log(1); } foo(); // 1 foo = function() { console.log(2); }
2. this
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
- this的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
- 当一个函数被调用时,会创建一个执行上下文,这个记录会包含函数的调用位置(函数调用栈),函数的调用反射光hi,传入的参数。
绑定规则:
-
默认绑定
- 如果直接调用函数,this指向全局对象
- 如果把null或undefined作为this的绑定对象传入call或applu,这些值在调用时会被忽略。实际应用默认绑定规则
如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined:
-
隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
回调函数丢失this绑定:
function foo() { console.log(this.a); } var obj = { a:2, foo:foo }; var a = "global"; setTimeout(obj.foo, 100); // global
-
显示绑定
call(…) apply() 方法
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是newString(…)、new Boolean(…)或者newNumber(…))。这通常被称为“装箱”。
-
硬绑定
bind() 返回一个硬编码的新函数,它会把指定的参数设置为this的上下文并调用原始函数
-
软绑定
-
-
new绑定
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
new调用函数的步骤:
- 创建一个新对象
- 执行prototype连接
- 绑定到函数调用共的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
-
优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
-
箭头函数不适用this的标准规则,而是根据外层(函数或全局)作用域来决定this,箭头函数最常用于回调函数中,例如事件处理器或者定时器, 相当于self=this
3. 对象
null有时会被当作一种对象类型,但是这其实只是语言本身的一个bug,即对null执行typeof null时会返回字符串"object"——二进制前三位000
-
在对象中,属性名永远都是字符串。如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。
-
ES6增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式来当作属性名
-
所以ES6定义了Object.assign(…)方法来实现浅复制。Object.assign(…)方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key,很快会介绍)并把它们复制(使用=操作符赋值)到目标对象
var newObj = Object,assign({},myObject);
-
属性描述符
Object.defineProperty(…)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。
- writable决定是否可以修改值
- Configurable决定是否可以使用defineProperty方法来修改属性描述符(把configurable修改成false是单向操作,无法撤销),configurable:false还会禁止删除
- Enumerable决定属性是否可枚举(是否出现在for…in循环中)
- 对象的不变性:使用writable:false和configurable:false创建常量属性; 使用Object.prevent Extensions()来禁止扩展; Object.seal(…)密封对象(密封后不能添加新属性,也不能重新配置或删除现有属性,前两者的结合); Object.freeze(…)冻结对象(seal+writable:false)
-
in操作符会检查属性是否在对象及其[[Prototype]]原型链中(参见第5章)。相比之下,hasOwnProperty(…)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
-
看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要,4in [2, 4, 6]的结果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是0、1、2,没有4。
-
访问属性时,引擎实际上会调用内部的默认[[Get]]操作(在设置属性值时是[[Put]]), [[Get]]操作会检查对象本身是否包含这个属性,如果没找到的话还会查找[[Prototype]]链
4. 类
- 混入(继承)
-
显式混入
现在我们来分析一下mixin(…)的工作原理。它会遍历sourceObj(本例中是Vehicle)的属性,如果在targetObj(本例中是Car)没有这个属性就会进行复制
function mixin(sourceObj, targetObj) { for(var key in sourceObj) { // 只会在不存在的情况下复制,子类对父类属性的重写 if(! (key in target)) { tergetObj[key] = sourceObj[key]; } } return targetObj; }
寄生继承
function Vehicle() { this.engines = 1; } Vehicle.prototype.igition = function() {}; Vehicle.prototype.frive = function() {}; // 寄生类 function Car() { var car = new Vihicle(); car.wheels = 4; var vehDrive = car.drive; car.drive = funciton() { vehDrive.call(this); }; return car; }
-
隐式混入
通过在构造函数调用或者方法调用中使用Something.cool.call(this),我们实际上“借用”了函数Something.cool()并在Another的上下文中调用了它(通过this绑定;参见第2章)。最终的结果是Something.cool()中的赋值操作都会应用在Another对象上而不是Something对象上
var Something = { cool: function() { this.greeting = "Hello World"; this.count = this.count ? this.count+1 : 1; } } Something.cool(); var Another = { cool: function() { // 隐式把Something混入Another Something.cool.call(this); } } Another.cool();
-
5. 原型
-
使用for…in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到(并且是enumerable,参见第3章)的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)
-
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype
-
属性设置和屏蔽
myObject.foo = "bar";
- foo直接存在于myObject中,直接赋值
- 在原型链上存在foo,直接在myObject中添加一个名为foo的新属性,它是屏蔽属性
- 若原型链上的foo被标记为只读(writable:false),严格模式下报错,否则这条语句会被忽略
- 若原型链上的foo是一个setter,那就一定会调用这个setter,不会屏蔽
-
换句话说,在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
-
constructor并不表示被构造
function Foo() {} Foo.prototype = {} var a = new Foo(); a.constructor === Foo; // fasle a.constructor === Object; // true
a并没有.constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是这个对象也没有.constructor属性(不过默认的Foo.prototype对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的Object.prototype。这个对象有.constructor属性,指向内置的Object(…)函数
-
调用Object.create(…)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象
if(! Object.create) { Object.create = function(o) { function F() {} F.prototype = o; return new F(); } }
-
检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)
-
instanceof操作符的左操作数是一个普通的对象a,右操作数是一个函数Foo()。instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象
-
Object.create(null)会创建一个拥有空(或者说null)[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符(之前解释过)无法进行判断,因此总是会返回false。这些特殊的空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。