《YDKJS 上卷》读书笔记
第一部分 作用域与闭包
作用域
为 JS 引擎提供支持,主要做以下 2 件事:
- 收集维护所声明的标识符(变量、函数)组成的一系列查询
- 确定当前代码对这些标识符的访问权限
JS 引擎主要通过LHS
和RHS
从内向外(作用域可以嵌套)访问作用域。

LHS 找到标识符(变量)容器本身,然后引擎给它赋值
RHS 则是找到该容器的值,引擎使用该值进行其他操作
查询结果:
- RHS 找不到,抛出
ReferenceError
异常 - LHS 找不到
- 非严格模式下在全局作用域创建同名变量
- 严格模式抛出
ReferenceError
异常
另,TypeError
异常表示作用域查询成功了但是对结果的操作是非法的,比如对不可调用对象执行调用操作、引用null
或undefined
的属性等。
词法作用域
词法作用域即是定义在词法阶段的作用域,由函数和代码块静态确定。
欺骗 JS 词法作用域
- eval()函数接收一个或多个声明的代码就会修改其所处的词法作用域
- with 声明根据传递给它的对象生成一个新的词法作用域
由于会导致 JS 引擎优化失灵和性能下降,以上方法在严格模式下被禁止
提升
函数和变量声明都会被提升,但是函数优先被提升,然后才是变量。
作用域闭包
1 | function foo() { |
闭包使函数在定义时的词法作用域以外被调用时还能访问定义时的词法作用域,闭包是对定义时词法作用域的引用。
1 | for (var i = 1; i <= 5; i++) { |
为什么全是 6?
- 定时器回调是在循环结束后才开始执行的
- 尽管每次函数都有重新定义,但是他们所在的外层作用域是相同的,这个作用域中只有一个
i
。 - 循环结束时,外层作用域中的
i
被赋值为 6
如何解决:创建一个新的作用域,用来存放每次迭代的i
。
1 | for (var i = 1; i <= 5; i++) { |
动态作用域
动态作用域不关心函数和作用域是在什么地方声明的,只关心它们是在何处调用的,即动态作用域链是基于调用栈的,而不是代码中的作用于嵌套。
动态作用域与词法作用域的区别:
- 词法作用域在定义(编码)时确定,动态作用域在运行时确定
- 词法作用域关注在何处声明,动态作用域关注在何处调用
事实上 Javascript 并不具有动态作用域,它只有词法作用域,简单明了,但是 this 机制在某种程度上很像动态作用域!
第二部分 this 和对象原型
this
在 Javascript 中,this 是一个很特别的关键字,它被自动定义在所有函数的作用域中。
this 在任何情况下都不指向函数的词法作用域,因为词法作用域是引擎内部的对象(可见的标识符都是它的属性),Javascript 用户代码无法访问它。
1 | function foo() { |
this 绑定规则
graph LR A[this绑定规则] -- 独立调用 --> B["默认绑定"] B -- 严格模式 --> C[绑定到undefined] B -- 非严格模式 --> D[绑定到全局对象] A -- 当作对象属性调用 --> E["隐式绑定"] --> F[绑定到这个对象] E -- var a = obj.fn --> G["丢失隐式绑定,应用默认绑定"] E -- obj.fn作为函数参数 --> G A -- "fn.call()" --> H["显式绑定"] A -- "fn.apply()" --> H H -- "call()、apply()用函数包装防止绑定丢失" --> L["硬绑定bind()"] A -- "使用new来调用函数" --> M["new绑定"] A -- "箭头函数=>" --> N["箭头函数绑定"]
优先级
绑定优先级自上而下递增
new 绑定
在 Javascript 中,“构造函数”只是一些普通的函数,它们并不属于某个类,也不会实例化一个类,它们只是被
new
操作符调用而已。
使用new
来调用函数 fn,Javascript 引擎内部会执行以下操作:
- 创建一个全新对象
- 执行原型链[[prototype]]链接
- 将该对象绑定到函数 fn 调用的 this
- 如果函数 fn 没有返回其他对象,那么返回第一步新建的对象
箭头函数绑定
在 ES6 中,箭头函数绑定会根据外层的词法作用域来决定 this,取代了传统的 this 机制,与var self = this;
异曲同工。
如果经常编写 this 风格代码,但是绝大部分时间都在使用
var self = this;
或箭头函数来否定 this 机制,那你或许应该:
- 只使用词法作用域并完全抛弃错误 this 风格代码。
- 完全采用 this 风格,在必要时使用 bind(),尽量避免使用
var self = this;
或者箭头函数。不要混用!!!
对象
Javascript 有六种类型:string、number、boolean、null、undefined、object,前五种是基本类型。
null 是基本类型,那么为什么typeof null => object
?
这是 Javascript 的一个 Bug,在 Javascript 底层不同类型都以二进制表示,Javascript 规定二进制前三位都是 0 的话判断为对象,但是 null 的二进制表示全是 0 所以错把它当作了 object。
属性不变性
- 不可修改:结合
writable: false
和configurable: false
可以创建一个真正的常量属性 - 禁止扩展:
Object.preventExtensions(obj);
禁止给对象添加新的属性 - 密封:
Object.seal(obj)
实际效果为 Object.preventExtensions() + configurable: false; - 冻结:
Object.freeze(obj)
实际效果为不可修改、禁止扩展、密封;只对当前对象有效,对这个对象引用的其他对象无效
getter、setter
1 | var myObj = { |
属性是否存在
方式 | 是否检查原型链 | 是否检查可枚举 | 备注 |
---|---|---|---|
in | 是 | 否 | |
hasOwnProperty() | 否 | 否 | |
propertyIsEnumerable() | 否 | 是 | |
Object.keys() | 否 | 是 | 返回一个数组,包含传入对象的所有可枚举属性 |
Object.getOwnPropertyNames() | 否 | 否 | 返回一个数组,包含传入对象的所有属性 |
1 | var otherObj = { |
一般来说,所有普通对象的原型链顶端都是
Object.prototype
,可以通过委托调用hasOwnProperty()
。但是也有特例,比如var obj = Object.create(null)
,obj
对象没有连接到Object.prototype
无法调用hasOwnProperty()
。
这种情况下,可以通过Object.prototype.hasOwnProperty.call(obj, 'xxx')
来调用。
属性遍历
键的遍历:
for .. in
只遍历对象(包括原型链)可枚举属性的键,需要值可以手动通过对象属性访问操作符获取
1 | var otherObj = { |
值的遍历:
for .. of
需要对象拥有@@iterator
一个返回迭代器对象的函数才能使用,数组原生内置,其他普通对象需要定义这个函数:
1 | var otherObj = { |
虚假的 JS 类 & 原型 & 原型链
类的继承本质是复制
委托:JS 中不存在真正的类!!! JS 只存在对象,一个对象不会被复制到另外一个对象中,它们只可以通过原型链做关联,这种设计模式称为委托而非类。
委托是一种极其强大的设计模式,和父类、子类、继承、多态等概念完全不同。委托中的对象并不是按照父类、子类的关系垂直组织的(父子关系),而是通过任意方向的委托关联并排组织的(兄弟关系、互相委托)。
JS 通过混入(mixin)来模拟类的复制,其本质就是复制一个对象的属性到另外一个对象,但是 JS 中的函数无法做到真正的复制,只能复制这个函数对象的引用。
JS 提供的class
关键字是语法糖,本质也是利用的[[prototype]]
,它本意是让问题理解起来更容易(类模式),但实际上却让 JS 更加难以理解。
JS 是一门采用[[prototype]]
机制的语言,对类的各种模拟始终会让人感到别扭、混乱。
原型 & 原型链
JS 对象有一个特殊的属性原型[[Prototype]]
,保存对其他对象的引用(委托)。如果在对象上没有找到需要的属性或者方法引用,JS 引擎就会继续在[[prototype]]
关联的对象上查找。同理,如果在后者中没有找到就继续查找后者[[prototype]]
所关联的对象,以此类推,这一系列对象的链接被称为原型链。
graph LR A[objA] -- "[[prototype]]" --> B[Object.prototype] C[objB] -- "[[prototype]]" --> A
所有普通的[[prototype]]
链最终都会指向内置的Object.prototype
对象。
1 | console.log(Object.prototype.hasOwnProperty("toString")); // => true |
对象也可以不具有原型属性,比如使用
Object.create(null)
创建的对象,这种不受原型链干扰的对象适合用做字典类型保存数据。
对象属性设置的影响
1 | objA.foo = "bar"; |
graph TD B{"objA.foo?"} --> |不存在| D["遍历原型链查找foo"] B --> |存在| C["直接修改foo的值"] D --> |找不到| E["objA添加foo"] D --> |找到| F{"foo有setter?"} F --> |有| G["直接调用setter"] F --> |无| H["只读?"] H --> |否| E H --> |是| I["抛出错误"]
- 如果原型链中和
objA
中同时存在foo
,那么原型链中的所有foo
属性会被objA.foo
屏蔽。- 原型链的只读属性会阻止链下层隐式创建同名属性,这么做主要是为了模拟类的继承。这个限制主要发生在属性直接赋值的情况(=),使用
defineProperty()
并不会受到影响。
一些针对对象原型的方法
Foo、Bar 为函数对象,a、b、c 为普通对象。访问函数对象的[[prototype]]
使用Foo.prototype
,普通对象可以使用a._prototype_
(不推荐)
设置:
方法 | 是否创建新对象 | 备注 |
---|---|---|
Foo.prototype = Bar.prototype | 否 | 实际是引用 |
Bar.prototype = new Foo() | 是 | 函数 Foo 可能有副作用 |
Bar.prototype = Object.create(Foo.prototype) | 是 | 原有对象被垃圾回收,性能略影响 |
Object.setPrototypeOf(Bar.prototype, Foo.prototype) | 否 | ES6、直接修改 |
获取&判断:
方法 | 说明 |
---|---|
a instanceof Foo | 在 a 的整个原型链中是否有指向 Foo.prototype 的关联 |
b.isPrototypeOf© | b 是否出现在 c 的原型链中 |
Object.getPrototypeOf(a) | ES5 获取 a 的[[prototype]] |
1 | var otherObj = { |
对象关联风格代码
下面是利用 JS 原型链关联(委托)的方式设计的 UI 控件代码。
ES5
1 | var Widget = { |
ES6
1 | var Widget = { |
总结:
- 在 API 接口设计中,委托细节最好在内部实现不要暴露出去。
- 从设计模式的角度,关联对象中最好不要有同名方法,定义的方法名应该更加描述性
- 避免丑陋的显示伪多态调用
Widget.prototype.init.call()
,充分利用原型链以委托的方式调用this.init()
- 不必出现
new
、.prototype
和任何构造函数,对象关联风格比类风格更加简洁、直观