你不知道的 JavaScript 上卷 第二部分 笔记
第1章 关于 this
1.1 为什么要用 this
this
提供了一种更优雅的方式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
var obj = {
name: 'Reader',
speak: function() {
console.log(this.name);
}
};
obj.speak(); // Reader
1.2 误解
有两种对this
常见的误解:
- 指向自身
this
的作用域
// 第一个误解: this 指向自身
function foo(){
console.log(this.count);
}
foo.count = 4;
var count = 3;
foo();
this
并不指向foo
函数,而是查找外层作用域,最终找到全局作用域的count
。
// 第二个误解:this 指向函数的作用域
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // undefined
首先,foo
函数向外层作用域找到bar
函数,然后逐层向外找a
,到全局作用域找到window
对象,然后window
上没有a
属性,所以是undfined
。
注意:this
在任何情况都不指向函数的词法作用域。在 JavaScript 内部,作用域和对象很相似,但是作用域无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。
1.3 this 到底是什么
this
是在运行时绑定的,不是在编写时绑定,它取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(也称执行上下文)。这个记录包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数信息。this
就是这个记录的一个属性,会在函数执行过程中用到。
第2章 this 全面解析
2.1 调用位置
分析调用位置最重要的是分析调用栈。

2.2 绑定规则
2.2.1 默认绑定
当没有其他绑定时,使用默认绑定,非严格模式下,this
绑定到全局作用域下的全局对象window
;严格模式下,不能使用全局对象,因此this
会绑定到undefined
。
function foo() {
console.log(this.a);
}
var a = 1;
foo(); // 1
2.2.2 隐式绑定
查看调用位置是否有上下文对象,或者说是否被某个拥有或者包含,如果是,this
“相当于”那个对象的引用。
function foo() {
console.log(this.a)
}
var a = 1;
var obj = {
foo: foo,
a: 2
}
obj.foo(); // 2
foo(); // 1
var foo1 = obj.foo;
foo1(); // 1
1
undefined
此时将obj
的foo
方法赋给foo1
, 此时调用foo1
相当于直接调用foo
。
2.2.3 显式绑定
如果我们想使用一个对象上的方法,并在某个对象上使用,这个时候就需要显式绑定,用到call
方法和apply
方法。
具体用法:Function.prototype.apply、Function.prototype.call
1. 硬绑定
硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind
。
// TODO: 理解更为复杂的bind的实现
// Function.prototype.bind 的 Polyfill(来自MDN)
// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
var slice = Array.prototype.slice;
Function.prototype.bind = function() {
var thatFunc = this, thatArg = arguments[0];
var args = slice.call(arguments, 1);
if (typeof thatFunc !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - ' +
'what is trying to be bound is not callable');
}
return function(){
var funcArgs = args.concat(slice.call(arguments))
return thatFunc.apply(thatArg, funcArgs);
};
};
})();
2. API 调用“上下文”
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选参数,通常被称为“上下文”,其作用和bind
一样。
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: 'awesome'
};
[1, 2, 3].forEach(foo, obj);
// 1 'awesome'
// 2 'awesome'
// 3 'awesome'
2.2.4 new 绑定
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建一个全新的对象
- 这个新对象会执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个对象
2.3 优先级
new
> 显式绑定 > 隐式绑定 > 默认绑定
显然,隐式绑定优先级大于默认绑定,因为如果默认绑定优先级大于隐式绑定,则通过对象调用方法时会绑定全局对象而不是绑定该对象。
function foo() {
console.log(this.a)
}
var obj = {
foo: foo,
a: 2
}
var obj1 = {
a: 3
}
obj.foo.call(obj1); // 3
上面的例子说明,显式调用优先级大于隐式调用。最后我们需要判定new
与显式绑定和隐式绑定的优先级。
function foo(something){
this.a = something;
}
var obj1 = {
foo: foo
};
obj1.foo(2);
var obj2 = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(obj2.a); // 4
可以看到new
绑定比隐式绑定优先级要高。
注意:new
和call/apply
无法一起使用,因此无法直接测试,但是可以通过硬绑定测试。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
bar
硬绑定在obj1
上,但是new bar(e)
并没有修改obj1
,而是绑定到一个新的对象。
2.4 绑定例外
2.4.1 被忽略的this
如果把null
或者undefined
作为绑定对象传给call/apply
或者bind
,这些值在调用是会被忽略,实际应用的是默认绑定规则。
一种常见做法是使用apply
来“展开”一个数组,当作参数传入一个函数。类似的,bind
可以对参数柯里化,这种方法有时非常有用。
function foo(a, b) {
console.log('a:', a, 'b:', b);
}
foo.apply(null, [2, 3]); // a: 2 b: 3
var bar = foo.bind(null, 2);
bar(3); // a: 2 b: 3
总是使用null
作为绑定对象,会有一些潜在的副作用,如果某个函数使用了this
,那么默认绑定规则会把this
绑定到全局对象,这会导致不可预测的结果。
更安全的this
更安全的办法是传入一个特殊的对象,把this
绑定到这个对象不会有任何副作用。
我们可以在忽略this
绑定时传入一个空对象,不会对全局对象产生影响。在 JavaScript 中创建一个空对象最简单的方法是Object.create(null)
,Object.create(null)
和null
很像,但是它并不会创建Object.prototype
这个委托,所以它更“空”。
function foo(a, b) {
console.log('a:', a, 'b:', b);
}
var empty = Object.create(null);
foo.apply(empty, [2, 3]); // a: 2 b: 3
2.4.2 间接引用
function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo}
var p = { a: 4 }
o.foo(); // 3
(p.foo = o.foo)(); // 2
(p.foo...)
返回的是foo
函数,所以会使用全局对象的a
。
2.4.3 软绑定
// TODO:软绑定的实现
2.5 this词法
箭头函数不使用this
的四种标准,而是根据外层作用域来确定this
。
function foo() {
return (a) => {
conosle.log(this.a)
}
};
var obj1 = {
a: 2
};
var obj2 = {
a: 3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2 而不是3
在foo.call(obj1);
运行完毕时,箭头函数的this
已经绑定在obj1
上,已经无法通过硬绑定重新绑定。
第3章 对象
3.1 语法
var myObject = {
key: 'value'
}
// 或
var myObj = new Object();
myObject.key = 'value';
3.2 类型
在JavaScript中有六种主要类型:
- string
- number
- boolean
- null
- undefined
- object
(ES6中,新加了symbol
)
简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。null有时会被当成一种对象类型,但是这是语言本身的一个bug,实际上,null本身是基本类型。
内置对象
JavaScript中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
在JavaScript中,他们实际上是一些内置函数,这些函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。
var strPrimitive = "I'm a string";
typeof strPrimitive; // string
strPrimitive instanceof String; // false
var strObject = new String("I'm a string");
typeof strObject; // 'object'
strObject instanceof String; // true
**注意:原始值I'm a string
不是一个对象,他只是一个字面量,并且不是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要转换为String对象,幸好在必要时语言会自动把字符串字面量转换成一个String对象。**number存在类似行为。
null
和undefined
没有对应的构造式,只有文字形式。相反Date
只有构造,没有文字形式。
对于Object
、Array
、Function
和RegExp
来说,无论使用文字形式还是构造形式,它们都是对象。
Error
对象很少在代码中显式创建,一般是在抛出异常时被自动创建。
3.3 内容
键访问obj['key']
,属性访问obj.key
。
3.3.1 可计算属性名
通过表达式来计算属性名,可以使用obj[perfix+name]
使用。
3.3.2 属性与方法
3.3.3 数组
数组也是对象,虽然每个下标都是整数,仍然可以给数组添加属性。
3.3.4 复制对象
对象拷贝分为深拷贝和浅拷贝,浅拷贝其实只是对原有对象的引用,原对象发生改变则浅拷贝的对象也会发生变化。
var obj = {
a: 1,
b: 2,
c: 3
}
var obj1 = obj;
obj1.a; // 1
obj.a = 2;
obj1.a; // 2
深拷贝比浅拷贝麻烦得多,JavaScript有一种办法实现深拷贝。
var obj = {a:3};
var newObj = JSON.parse(JSON.stringify(obj));
newObj.a; // 3
obj.a = 2;
newObj.a; // 3
但是这种方法的前提是保证对象是JSON安全的,所以只适用部分情况。
尽管,JavaScript的Object上有assign方法,他可以进行对象间的复制,但是仍然不满足深拷贝的要求。 Object.assign的详细信息:Object.assign() - JavaScript | MDN
var obj = {
a: 1,
b: {
c: 3
}
}
var newObj = Object.assign({}, obj);
obj.a = 2
newObj.a // 1
obj.b.c = 4;
newObj.b.c; // 4
尽管,复制出了一个newObj
,但是它内部的b
属性还是引用的obj
内部的b
属性,还是浅拷贝。
3.3.5 属性描述符
Object.getOwnPropertyDescriptor(obj, prop)获取属性描述符。 Object.defineProperty(obj, prop, descriptor)添加一个新属性或者修改一个已有属性。
3.3.6 不变性
1.对象常量
结合writeable: false
和configurable:false
就可以创建一个真正的常量属性(不可被修改、重定义或者删除)
2.禁止扩展
Object.preventExtensions(obj)禁止一个对象添加新属性并且保留已有属性。
3.密封
Object.seal在禁止扩展基础上,把现有属性标记为configurable:false
4.冻结
Object.freeze在密封的基础上,把所有属性标记为writable:false
**注意:**这些功能只能作用在一个对象的键上,但是如果某一个键的值是一个对象,该对象不会受到影响,即嵌套对象内部的对象的可变性不受影响。如果像深度修改,逐级遍历内部的对象。
3.3.7 [[Get]]
var myObject = {
a: 2
};
myObject.a;
在语言规范中,myObject.a
在myOjbect
上实际上实现了[[Get]]
操作,首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性,否则就会在原型链上寻找。
TODO:原型链见后文
3.3.8 [[Put]]
有获取属性的操作,自然有对应的[[Put]]
操作,[[Put]]
算法大致会检查下面这些内容:
- 属性是否是访问描述符(参见3.3.9节)?如果是并且存在
setter
就调用setter
- 属性的数据描述符中
writable
是否是false
?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError
异常 - 如果不是,将该值设置为属性的值
如果对象中不存在这个属性,[[Put]]
操作会更加复杂,TODO:在后文讨论。
3.3.9 Getter和Setter
在ES5中可以使用getter
和setter
部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter
是一个隐藏函数,会在获取属性值时调用。setter
也是一个隐藏函数,会在设置属性值时调用。
var myObject = {
get a() {
console.log('this is getter');
return 2;
}
}
myObject.a;
// this is getter
// 2
Object.defineProperty(
myObject,
"b",
{
get: function(){ return this.a * 2 },
enumerable: true,
configurable: true
}
);
myObject.b;
// this is getter
// 4
为了让属性更合理,还应当定义setter
,和你期望的一样,setter
会覆盖单个属性默认的[[Put]]
操作。通常来说getter
和setter
是成对出现的(只定义一个的话通常会产生意料之外的行为):
var myObject = {
get a() {
return this._a_;
},
set a(val) {
this._a_ = val * 2;
}
}
myObject.a = 2;
myObject.a; // 4
3.3.10 存在性
访问一个对象个属性返回为undefined
,但是这个属性是不存在,还是存在但是值是undefined
,如何区分这两种情况?
我们可以在不访问属性值情况下判断对象中是否存在这个属性:
var myObject = {
a: 2
};
"a" in myObject; // true
"b" in myObject; // false
myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
in
操作符会检查属性是否咋爱对象及其[[Prototype]]
原型链中。TODO:参见第5章。相比之下,hasOwnProperty
只会检查属性是否在myObject
中,不会检查原型链。
Object.keys()
返回对象直接包含的所有可枚举属性,Object.getOwnPropertyNames()
返回对象直接包含的所有属性,无论是否可枚举。
3.4 遍历
for...in
循环可以用来遍历对象的可枚举属性列表(包括原型链)。
for(var i = 0; i< len; i++){...}
的方式其实不是在遍历值,而是用下标来指向值然后访问。
forEach
会遍历数组所有值,并返回回调函数的返回值。
every
一直运行回调函数返回fasle
some
一直运行到回调函数返回真值
这部分省略for..of
的内容,这部分直接看ES6的教程更好些,后面会省略部分ES6的内容。
第4章 混合对象“类”
类/继承描述了一种代码组织结构形式——一种对真实世界中问题领域的建模方法。
举个例子,以汽车为例:
类 就是图纸,图纸上包含了它的各种零件以及它具备什么样的功能。
实例化就是按照图纸造一辆车出来。
// 伪代码
calss Vehicle{
engines = 1
run() {
console.log('run')
}
toot() {
console.log('toot')
}
}
类的实例化是由一个特殊的方法来构造的,这个方法被称为构造函数,通过new
来调用。别的语言中,这个方法名和类名相同。JavaScript比较特殊,后面会说明。
类一个很重要的特性就是继承,假设有一个父类,一个子类,子类继承父类,父类的特性会复制给子类。 用车辆举例,交通工具是父类,小轿车是子类,它继承了交通工具的所有特性。 类另一个重要的特性是多态,用交通工具举例,交通工具和小轿车都可以驾驶,但是小轿车是四轮驱动的行驶,小轿车的类定义了自己行驶方法,行驶时会使用自身的行驶方法,而不是父类的。
calss Car inherits Vehicle{
run() {
console.log('car run')
}
}
多重继承就是继承多个父类,但是JavaScript本身不提供多重继承。
4.4 混入
在JavaScript中,只存在对象,不存在类,在其他语言中类表现出来的都是复制行为,因此JavaScript开发者也想出一个方法来模拟类的复制行为,这个方法就是混入。
第5章 原型
5.1 [[Prototype]]
JavaScript 中又一个特殊的[[Prototype]]
内置属性,其实就是对其他对象的饮用。几乎所有的对象在创建时[[Prototype]]
的属性都会被赋予一个非空的值。
然而虽然说[[Prototype]]
是一个隐藏属性,但很多浏览器都给每一个对象提供__proto__
这一属性,这个属性就是上文反复提到的该对象的[[prototype]]
。由于这个属性不标准,因此一般不提倡使用。
在第3章中说过,当试图引用对象的属性时会触发[[Get]]
操作,比如myObject.a
,对于默认的[[Get]]
操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。
但是如果不存在,就需要使用对象的[[Prototype]]
链了。for...in
同理。
var obj = {
a: '1'
};
var source = {
b: 'source: 2'
};
obj.__proto__ = source; // 不推荐这么使用
obj.b; // source: 2
source.b = 'source: 22';
obj.b; // source: 22
'source: 22'
var source = {
b: 'source: 2'
};
var obj = Object.create(source);
obj.b; // source: 2
5.1.1 Object.prototype
哪里是[[Prototype]]
链的“尽头”?
所有普通的[[Prototype]]
最终都会指向内置的Object.prototype
。


5.1.2 属性的设置和屏蔽
属性访问时,如果不直接存在对象上时,会遍历原型链。如果,在原型链上遍历时,先发现的对象上有该属性,就取出该属性,不再查找后面的对象,就屏蔽了原型链后面的同名属性。
但是在设置属性时,会有出人意料的行为发生:
- 如果在原型链上存在该属性并且没有被标记为只读,那就直接在对象上添加该属性。
- 如果在原型链上存在该属性但被标记为只读,那么无法修改已有属性和在对象上添加新的属性。严格模式下,会抛出一个错误;否则这条语句会被忽略。
- 如果在原型链上存在该属性并且它是一个
setter
,那就会调用这个setter
,不会被添加到该对象,也不会在对象上重新定义一个setter
。
5.2 “类”
在JavaScript中,只有对象。
总结到 JavaScript 类这一块儿的时候,书里有很多理论性的内容,这部分需要单独总结,而且工作量还不小,所以等后面有时间了,会尝试总结一下。
转载自:https://juejin.cn/post/6844904020822212616