【读书笔记】《你不知道的 JavaScript(上卷)》
第一部分 作用域和闭包
1.1 作用域是什么?
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
JavaScript 引擎首先会在代码执行前对其进行编译(词法分析->语法分析->代码生成),在这个过程中,像var a = 2
这样的声明会被分解成两个独立的步骤:
- 首先,
var a
在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。 - 接下来,
a = 2
会查询(LHS 查询)变量 a 并对其进行赋值。
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说他们没有找到所需的标识符),就会向上级作用域继续查找标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将会停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。
1.2 词法作用域
词法作用域意味着作用域是由书写代码时函数声明的位置决定的。编译的词法分析阶段基本能够知道全部标识符在那里以及是如何声明的,从而能够预测在执行过程中如何对他们进行查找。
JavaScript 中有两个机制可以“欺骗”词法作用域:
eval(...)
:可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。with
:本质上是通过将一个对象的引用当做当前作用域来处理,将对象的属性当做作用域中的标识符来处理,从而创建一个新的词法作用域(同样是在运行时)。
上述两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用其中任意一个机制都将导致代码运行变得缓慢。不要使用他们!!!
1.3 函数作用域和块作用域
函数是 JavaScript 中最常见的作用域单元。本质上,声明一个函数内部的变量或函数,会在其所处的作用域中“隐藏”起来,这是有意为之的良好的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{...}
内部)。
从 ES3 开始,try/catch
结构在 catch 分句中具有块作用域。
在 ES6 引入了 let 关键字,用来在任意代码块中声明变量;const 关键字声明常量。
1.4 提升
我们习惯将var a = 2
;看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将var a
和a = 2
当做两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。(回顾下:编译-解析-执行)
因此无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式(var a = function(){...}
)的赋值在内的赋值操作并不会提升。
在写代码时我们要避免重复声明!
1.5 作用域闭包
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
闭包可以用多种形式来实现模块等模式。
模板有两个主要特征:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一个队内部函数的引用,这样就会创建覆盖整个包装函数内部作用域的闭包。
附录A-动态作用域
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心他们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
词法作用域和动态作用域的区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
附录B-块作用域的替代方案
ES6之前是使用 catch 实现:
try{trow undefined}catch(a){
a = 2;
consloe.log(a); // 2
}
第二部分 this 和对象原型
2.1 关于 this
this
是在运行时进行绑定的,并不是在编写时绑定的,它的上下文(context)取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数调用方式,它指向什么完全取决于函数在哪里被调用。
2.2 this 全面解析
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由
new
调用:绑定到新创建的对象。 - 由
call
调用或者apply
(或者bind
)调用:绑定到指定的对象。(显式绑定) - 由上下文对象(作为对象的方法)调用:绑定到那个上下文对象。(隐式绑定)
- 默认(作为独立函数调用):在严格模式下绑定到 undefined,否则绑定到全局对象 window。
ES6 的箭头函数是根据词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这和 ES6 之前代码中的self = this
机制一样。
2.3 对象
JavaScript 中的对象有字面量形式(var a = {...}
)和构造形式(var a = new Array(...)
)。字面形式更常用,不过有时候构造形式可以提供更多选项。
许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者说 7 个,取决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]
表示这是对象的子类型数据。
对象就是键/值对的集合。可以通过.propName
或者["propName"]
语法来获取属性值。访问属性时,引擎实际上会调用内部的默认[[Get]]
操作(在设置属性时是[[Put]]
),[[Get]]
操作会检查对象本身是否包含这个属性吗,如果没有找到的话还会查找[[Prototype]]
链。
属性的特性可以通过属性描述符来控制,比如 writable 和 configurable。此外,可以使用 object.preventExtensions(...)、Object.seal(...) 和 Object.freeze(...)
来设置对象(及其属性)的不可变性级别。
属性不一定包含值——他们可能是具备 getter/setter 的“访问描述符”。此外属性可以是可枚举或者不可枚举的,这取决于他们是否会出现在 for..in
循环中。
ES6 的for..of
语法可以遍历数据结构(数组、对象)中的值。for..of 会寻找内置货值自定义的 @@intterator 对象并调用它的 next()
方法来遍历数据值。
2.4 混合对象“类”
类是一种设计模式。类意味着复制。传统的类实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到之类中。
多态(在继承链的不同层次名称相同但是功能不同的函数)看看起来似乎是从子类引用父类,但是本质上引用的是负复制的结果。
JavaScript 并不会(像类那样)自动创建对象的副本。
混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑类并且脆弱的语法,比如显式伪多态(OtherObj.methodName.call(this,....)
),这会让代码更加难懂并且难以维护。
此外,显式混入无法完全模拟类的复制行为,因为对象和函数只能复制引用,无法复制被引用的对象或者函数本身。
总结:在 JavaScript 中模拟类是得不偿失的!
2.5 原型
如果要访问对象中并不存在的一个属性,[[Get]]
操作就会查找对象内部的[[prototype]]
关联的对象。这个关联实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时,会对它进行遍历。
所有普通对象都有内置的Object.prototype
,指向原型链的顶端(比如说全局作用域),如果在原型链中找不到指定的属性就会停止。toString()、valueOf
和其他一些通用的功能都有存在于Object.prototype
对象上,因此语言中所有的对象都可以使用他们。
关联的两个对象最常用的方法是使用new
关键字进行函数调用,在调用的 4 个步骤(新建对象->修改__proto__
->call 调用->返回对象)中会创一个关联其他对象的新对象。
使用 new 调用函数时会把新对象的 .prototype 属性关联到其他对象。带 new 的函数调用通常被称之为“构造函数调用”,尽管他们实际上和传统的面向类语言中的类构造函数不一样。
虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但是 JavaScript 中的机制有一个核心的区别,那就是不会进行复制,对象之间是通过[[Prototype]]链关联的。
处于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无法帮助我们理解 JavaScript 的真实机制。
相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。
2.6 行为委托
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和字类得关系。Javascript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,吗可以选择在 Javascript 中努力实现类机制(组合式继承),也可以拥抱更自然的 [[Prototype]]委托机制(Object.create()
)。
当只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。
对象关联(对象之间互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把他们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。