0%

《YDKJS 上卷》读书笔记

第一部分 作用域与闭包

作用域

为JS引擎提供支持,主要做以下2件事:

  1. 收集维护所声明的标识符(变量、函数)组成的一系列查询
  2. 确定当前代码对这些标识符的访问权限

JS引擎主要通过LHSRHS从内向外(作用域可以嵌套)访问作用域。

LHS找到标识符(变量)容器本身,然后引擎给它赋值
RHS则是找到该容器的值,引擎使用该值进行其他操作

查询结果:

  1. RHS找不到,抛出ReferenceError异常
  2. LHS找不到
    1. 非严格模式下在全局作用域创建同名变量
    2. 严格模式抛出ReferenceError异常

另,TypeError异常表示作用域查询成功了但是对结果的操作是非法的,比如对不可调用对象执行调用操作、引用nullundefined的属性等。

词法作用域

词法作用域即是定义在词法阶段的作用域,由函数和代码块静态确定。

欺骗JS词法作用域

  1. eval()函数接收一个或多个声明的代码就会修改其所处的词法作用域
  2. with声明根据传递给它的对象生成一个新的词法作用域
    由于会导致JS引擎优化失灵和性能下降,以上方法在严格模式下被禁止

提升

函数和变量声明都会被提升,但是函数优先被提升,然后才是变量。

作用域闭包

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
let a = 2;

function baz() {
console.log(a);
}

return baz;
}

var baz = foo();
baz(); // => 2

闭包使函数在定义时的词法作用域以外被调用时还能访问定义时的词法作用域,闭包是对定义时词法作用域的引用。

1
2
3
4
5
6
for (var i = 1; i <=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}
// => 6 6 6 6 6

为什么全是6?

  1. 定时器回调是在循环结束后才开始执行的
  2. 尽管每次函数都有重新定义,但是他们所在的外层作用域是相同的,这个作用域中只有一个i
  3. 循环结束时,外层作用域中的i被赋值为6

如何解决:创建一个新的作用域,用来存放每次迭代的i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (var i = 1; i <=5; i++) {
(function(j){
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i) // 利用IIFE创建一个新的作用域并将i的值保存在这个作用域中
}

// 也可以使用let来创建块作用域
for (var i = 1; i <=5; i++) {
let j = i; // 创建块作用域并声明一个新变量存放i的值
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}

// 还可以将let放入for声明中,这样每次都会重新声明一个新的块作用域(let重新声明i并使用上一次迭代的值初始化这个i)
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
})
}

动态作用域

动态作用域不关心函数和作用域是在什么地方声明的,只关心它们是在何处调用的,即动态作用域链是基于调用栈的,而不是代码中的作用于嵌套。

动态作用域与词法作用域的区别:

  1. 词法作用域在定义(编码)时确定,动态作用域在运行时确定
  2. 词法作用域关注在何处声明,动态作用域关注在何处调用

事实上Javascript并不具有动态作用域,它只有词法作用域,简单明了,但是this机制在某种程度上很像动态作用域!

第二部分 this和对象原型

this

在Javascript中,this是一个很特别的关键字,它被自动定义在所有函数的作用域中。

this在任何情况下都不指向函数的词法作用域,因为词法作用域是引擎内部的对象(可见的标识符都是它的属性),Javascript用户代码无法访问它。

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
this.bar(); // 错误!!this为undefined
}

function bar() {
console.log(this.a); // 错误!! 不能用this来引用一个词法作用域中的变量
}

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引擎内部会执行以下操作:

  1. 创建一个全新对象
  2. 执行原型链[[prototype]]链接
  3. 将该对象绑定到函数fn调用的this
  4. 如果函数fn没有返回其他对象,那么返回第一步新建的对象

箭头函数绑定

在ES6中,箭头函数绑定会根据外层的词法作用域来决定this,取代了传统的this机制,与var self = this;异曲同工。

如果经常编写this风格代码,但是绝大部分时间都在使用var self = this;或箭头函数来否定this机制,那你或许应该:

  1. 只使用词法作用域并完全抛弃错误this风格代码。
  2. 完全采用this风格,在必要时使用bind(),尽量避免使用var self = this;或者箭头函数。

不要混用!!!

对象

Javascript有六种类型:string、number、boolean、null、undefined、object,前五种是基本类型。

null是基本类型,那么为什么typeof null => object
这是Javascript的一个Bug,在Javascript底层不同类型都以二进制表示,Javascript规定二进制前三位都是0的话判断为对象,但是null的二进制表示全是0所以错把它当作了object。

属性不变性

  1. 不可修改:结合writable: falseconfigurable: false可以创建一个真正的常量属性
  2. 禁止扩展:Object.preventExtensions(obj); 禁止给对象添加新的属性
  3. 密封:Object.seal(obj) 实际效果为Object.preventExtensions() + configurable: false;
  4. 冻结:Object.freeze(obj) 实际效果为不可修改、禁止扩展、密封;只对当前对象有效,对这个对象引用的其他对象无效

getter、setter

1
2
3
4
5
6
7
8
9
var myObj = {
get a() {
return this._a_;
}

set a(val) {
this._a_ = val * 2;
}
}

属性是否存在

方式 是否检查原型链 是否检查可枚举 备注
in
hasOwnProperty()
propertyIsEnumerable()
Object.keys() 返回一个数组,包含传入对象的所有可枚举属性
Object.getOwnPropertyNames() 返回一个数组,包含传入对象的所有属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var otherObj = {
b: 3,
}

var myObj = Object.create(otherObj);
myObj.a = 2;

// in 操作符检查当前对象以及它的原型链
console.log(('a' in myObj)); // => true
console.log(('b' in myObj)); // => true
// hasOwnProperty() 只检查当前对象
console.log(myObj.hasOwnProperty('a')); // => true
console.log(myObj.hasOwnProperty('b')); // => false

console.log(Object.keys(myObj)); // => [ 'a' ]
console.log(Object.getOwnPropertyNames(myObj)); // => [ 'a' ]

一般来说,所有普通对象的原型链顶端都是Object.prototype,可以通过委托调用hasOwnProperty()。但是也有特例,比如var obj = Object.create(null)obj对象没有连接到Object.prototype无法调用hasOwnProperty()
这种情况下,可以通过Object.prototype.hasOwnProperty.call(obj, 'xxx')来调用。

属性遍历

键的遍历:
for .. in 只遍历对象(包括原型链)可枚举属性的键,需要值可以手动通过对象属性访问操作符获取

1
2
3
4
5
6
7
8
9
10
11
12
var otherObj = {
b: 3,
}

var myObj = Object.create(otherObj);
myObj.a = 2;

for (var v in myObj) {
console.log(v);
}
// => a
// => b

值的遍历:
for .. of 需要对象拥有@@iterator一个返回迭代器对象的函数才能使用,数组原生内置,其他普通对象需要定义这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var otherObj = {
b: 3,
}

var myObj = Object.create(otherObj);
myObj.a = 2;

// 没有定义@@iterator
for (var v of myObj) {
console.log(v);
} // => TypeError: myObj is not iterable

Object.defineProperty(myObj, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var keys = Object.keys(o);
return {
next: function() {
return {
value: o[keys[idx++]],
done: (idx > keys.length),
}
}
}
},
})

// 使用for .. of 遍历
for (var v of myObj) {
console.log(v);;
} // => a

虚假的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
2
console.log(Object.prototype.hasOwnProperty('toString')); // => true
console.log(Object.prototype.hasOwnProperty('valueOf')); // => 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["抛出错误"]
  1. 如果原型链中和objA中同时存在foo,那么原型链中的所有foo属性会被objA.foo屏蔽
  2. 原型链的只读属性会阻止链下层隐式创建同名属性,这么做主要是为了模拟类的继承。这个限制主要发生在属性直接赋值的情况(=),使用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
2
3
4
5
6
7
8
9
var otherObj = {
b: 3,
}
console.log(Object.getPrototypeOf(otherObj) === Object.prototype) // => true

function construct() {
console.log('xxxxx')
}
console.log(Object.getPrototypeOf(construct) === Function.prototype); // => true

对象关联风格代码

下面是利用JS原型链关联(委托)的方式设计的UI控件代码。

ES5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var Widget = {
init: function(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$ele = null;
},
insert: function(where) {
if(this.$ele) {
this.$ele.css({
width: this.width + 'px',
height: this.height + 'px',
}).appendTo(where);
}
}
}

// 新建一个对象并与Widget做关联
var Button = Object.create(Widget);

Button.setup = function(width, height, label) {
// 委托调用
this.init(width, height);
this.label = label || 'Default';
this.$ele = $('<button>').text(this.label);
}

Button.build = function(where) {
// 委托调用
this.insert(where);
this.$ele.click(this.onClick.bind(this));
}

Button.onClick = function() {
console.log('Button ' + this.label + ' clicked!')
}

$(document).ready(function(){
var $body = $(document.body);

var btn1 = Object.create(Button);
var btn2 = Object.create(Button);

btn1.setup(125, 340, 'hello');
btn2.setup(124, 111, 'world');

btn1.build(body);
btn2.build(body);
})

ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var Widget = {
init(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$ele = null;
},
insert(where) {
if (this.$ele) {
this.$ele.css({
width: this.width + 'px',
height: this.height + 'px',
}).appendTo(where);
}
}
}

var Button = {
setup(width, height, label) {
// 委托调用
this.init(width, height);
this.label = label || 'Default';
this.$ele = $('<button>').text(this.label);
},
build(where) {
// 委托调用
this.insert(where);
this.$ele.click(this.onClick.bind(this));
},
onClick() {
console.log('Button ' + this.label + ' clicked!')
},
}

// 关联两个对象
Object.setPrototypeOf(Button, Widget);

$(document).ready(function () {
var $body = $(document.body);

var btn1 = Object.create(Button);
var btn2 = Object.create(Button);

btn1.setup(125, 340, 'hello');
btn2.setup(124, 111, 'world');

btn1.build(body);
btn2.build(body);
})

总结:

  1. 在API接口设计中,委托细节最好在内部实现不要暴露出去。
  2. 从设计模式的角度,关联对象中最好不要有同名方法,定义的方法名应该更加描述性
  3. 避免丑陋的显示伪多态调用Widget.prototype.init.call(),充分利用原型链以委托的方式调用this.init()
  4. 不必出现new.prototype和任何构造函数,对象关联风格比类风格更加简洁、直观
请吃个烤串~~
willsky 微信

微信