读《你不知道的 JavaScript》
Part 1 作用域与闭包
词法作用域
定义在词法阶段的作用域。换句话说,词法作用域就是由你在写代码时将变量和块作用域写在哪里来决定的。
-
词法作用域只由函数声明所在之处决定
-
只会查找一级标识符,二级三级...都不会找下去
函数表达式&函数声明
-
(function(){...}()) 这是函数表达式,这个等同于 (function foo(){...})()
立即执行函数表达式
-
function 关键字如果是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式
块作用域
- try/catch catch 语句中有块作用域
提升
-
变量和函数在内的所有声明都任何代码被在执行前首先被处理(先有声明后有赋值)
-
函数声明比变量先被提升(重复的 var 声明会被忽略,重复的函数声明会覆盖原来的)
闭包
-
当函数可记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的
-
模块有两个主要特征:
(1)为创建内部作用域而调用了一个包装函数
(2)包装函数的返回值必须至少包括一个对内部函数的引用
这样子就创建了涵盖整个包装函数内部作用域的闭包
Part 2 this
- 具名函数可以使用名称标识符来引用自身; 而匿名函数没有名称标识符,无法从函数内部引用自身
function foo(num) {
console.log("foo: " + num);
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo.call(foo, i);
}
}
console.log(foo.count);
使用 call(...)可以确保 this 指向函数对象 foo 本身
- this 在任何情况下都不指向函数的词法作用域。this 实际上是在函数调用时发生的绑定。
this 既不指向函数自身也不指向函数的词法作用域,它的指向完全取决于函数在哪被调用(谁调用就指向谁)。
默认绑定
可以当作是无法应用其他规则时的默认规则。
独立函数调用
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2 (严格模式下, 是undefined)
细节: 虽然 this 的绑定规则完全取决于调用位置,但是只有 foo()运行在非 strict mode 下时, this 默认绑定才能绑定到全局对象;严格模式下,与 foo()的调用位置无关
隐式绑定
- 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
obj.foo();
- 隐式丢失
显式绑定
- 可以通过 call(...) 和 apply(...) 方法直接指定 this 的绑定对象。
call(...) 和 apply(...) 的第一个参数是一个对象,会把这个对象绑定到 this , 接着在调用函数时指定这个 this
如果传入的是一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成其对象形式(也就是 new String(...)、new Boolean(...)、new Number(...))。这通常称为“装箱”。
call(...) 和 apply(...) 的区别在于第二个参数:apply(...) 接受数组形式的参数,call(...) 接受的是参数列表
- 显示的强制绑定,称为硬绑定
ES5 提供了内置的方法 Function.prototype.bind , 用法如下:
function foo(value) {
console.log(this.a, value); // 2 3
return this.a + value;
}
var obj = {
a: 2,
};
var bar = foo.bind(obj);
var b = bar(3);
console.log(b); // 5
bind(...) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数
new 绑定
- 使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面操作:
- 1.创建(或者说构造)一个全新的对象
- 2.这个新对象会被执行原型连接
- 3.这个新对象会绑定到函数调用的 this
- 4.如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
优先级
显示绑定 > 隐式绑定
new 绑定 > 隐式绑定
- 在 new 中使用硬绑定函数:主要目的是预先设置函数的一些参数,这样子在使用 new 进行初始化时就可以只传其余的参数。
bind(...) 的功能之一就是可以把除了第一个参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)。
function foo(p1, p2) {
this.val = p1 + p2;
}
// 使用 null 是因为我们不关心 this 绑定了谁
var bar = foo.bind(null, "p1");
// 使用 new 时, this 会被修改了
var baz = new bar("p2");
console.log(baz.val); // p1p2
判断 this
可以按以下顺序判断:
- 1.函数是否在 new 中调用( new 绑定)?YES- this 绑定的是新创建的对象
var bar = new foo();
- 2.函数是否通过 call 、apply(显式绑定)或者硬绑定(bind)调用?YES- this 绑定的是指定的对象
var bar = foo.call(obj2);
- 3.函数是否在某个上下文对象中调用(隐式绑定)?YES- this 绑定的是那个上下文对象
var bar = obj1.foo();
- 4.以上都不是的话,使用默认绑定。严格模式下,就绑定到 undefined, 否则绑定到全局对象。
var bar = foo();
绑定例外
- 如果说,把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind ,这些值在调用时会被忽略,实际应用的还是默认绑定。
- 常见的做法是使用 apply(...) 来展开一个数组,并作为参数传入一个函数中:
function foo(a, b) {
console.log(a, b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // 2 3
- bind(...) 可以对参数进行柯里化(预先设置一些参数):
function foo(a, b) {
console.log(a, b);
}
// 使用 bind(...) 进行柯里化
var bar = foo.bind(null, 2);
bar(3); // 2 3
箭头函数
-
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
-
箭头函数的绑定无法修改( new 也不行!)
对象
- 简单基本类型(string number boolean undefined null)本身不是对象
typeof null 会返回 'object', 但实际上 null 是基本类型
-
函数就是对象的一个子类型(可调用的对象)
-
访问属性时,引擎实际上会调用内部默认的[[Get]]操作(在设置属性值时是[[Put]]),[[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]]链
内置对象
String Number Boolean Object Function Array Date RegExp Error
- 实际上,这只是一些内置函数,可以当作构造函数来使用,从而构造一个对应子类型的新对象
var strObject = new String("I am a string");
-
null 和 undefined 没有对应的构造形式,只要文字(声明)形式。 Date 只要构造形式,没有文字形式。
-
Object Array Function RegExp 无论是使用文字形式还是构造形式,他们都是对象,不是字面量。
复制对象
- Object.assign() 浅拷贝,是使用 = 操作符来赋值
存在性
-
in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中, hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
-
propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable:true
-
Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举
遍历
-
for...in 遍历的是对象中的所有可枚举属性,不是属性值
-
可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值,for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。
类
- 类意味着复制???
显式混入
- JS 中的函数无法真正的复制,只能复制对共享函数对象的引用(函数就是对象)
原型
当试图引用对象的属性时会触发[[Get]]操作。对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,有的话就使用它;没有的话就需要使用对象的[[Prototype]]链了。
-
[[Prototype]] 对于其他对象的引用,几乎所有的对象在创建时[[Prototype]]属性会被赋予一个非空的值
-
Object.create(..) 它会创建一个对象并把这个对象的 [[Prototype]] 关联到指定的对象,有个缺点是:需要创建一个新对象然后把就对象抛弃掉,不能直接修改已有的默认对象。第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符(enumerable writable configurable 等等)
Object.create(null) 会创建一个拥有空 [[prototype]] 链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以 instanceof 操作符无法进行判断,因此总会返回 false
-
所有普通的[[Prototype]]链最终都会指向内置的 Object.prototype
-
实际上,对象的.constructor 会默认指向一个函数,这个函数可以通过对象的.prototype 引用。constructor 并不表示被构造
-
Object.setPrototypeOf(...) 可以用标准且可靠的方法来修改关联
// ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
// ES6开始可以直接修改现有的Bar.prototype
Bar.setPrototypeOf(Bar, prototype, Foo.prototype);
属性设置和屏蔽
obj.foo = 2;
分析:
-
如果 foo 不是直接存在于 obj 中,[[Prototype]] 链就会被遍历,类似 [[Get]] 操作。如果原型链上找不到 foo,foo 就会被直接添加到 obj 上。
-
如果说,foo 既出现在 obj 中,也出现在 myObject 的 [[Prototype]] 链上层,那么就会发生屏蔽:obj 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性(因为 obj.foo 总会选择原型链中最底层的 foo 属性)
-
屏蔽属性???
(原型)继承
-
内省/反射:检查一个实例(JavaScript 中的对象)的继承祖先(JavaScript 中的委托关联)
-
判断[[Prototype]]反射的方法:isPrototypeOf(...)
Foo.prototype.isPrototypeOf(a); // true(在 a 的整条 [[Prototype]] 链中是否出现过 Foo.prototype?)
b.prototype.isPrototypeOf(c); // 判断 b 是否出现在 c 的 [[Prototype]] 中
- 获取一个对象的 [[Prototype]] 链:
// 在 ES5 中,标准方法:
Object.getPrototypeOf(a);
// 绝大多数浏览器也支持一种非标准的方法来访问内部 [[Prototype]] 属性:
a.__proto__; // .__proto__ 实际上并不存在与你正在使用的对象中(.constructor 也是一样的),实际上是存在于 Object.prototype 中
- instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上 ???
对象关联
- Object.create()的 polyfill 代码:
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
- 关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤中会创建一个关联其他对象的新对象。使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”
行为委托
-
[[prototype]] 机制就是指对象中的一个内部链接引用另一个对象
-
在委托行为中,我们会尽量避免在[[Prototype]]链的不同级别中使用相同的命名
-
委托行为意味着某些对象在找不到属性或者方法引用时,会把这个请求委托给另一个对象
-
在 API 接口的设计中,委托最好在内部实现,不要直接暴露出去
互相委托(禁止)
- 无法在两个或两个以上互相(双向)委托的对象之间创建循环委托。如果把 B 关联到 A 然后试着吧 A 关联到 B 就会出错