本文内容
本文主要是记录一些重点知识,包括原型链、NEW 运算符,this 引用等
原型链
记住以下 2 条即可:
JavaScript 对象有一个指向一个原型对象的引用(称之为
__proto__)。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型对象,以及该对象的原型对象的原型对象,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(一直找到 Object 到 null)。例如:var obj1 = { x: 1, name: function () { return 'obj1'; } }; var obj2 = { y: 2, name: function () { return 'obj2'; } }; var obj3 = { z: 3, name: function () { return 'obj3'; }, toString: function () { for (var x in this) { if (x !== 'name' && x !== 'toString') { console.log('Object: ' + this.name() + ', ' + x + ': ' + this[x]); } } } }; obj1.__proto__ = obj2; obj2.__proto__ = obj3; // 经过以上操作,现在 obj1 的原型对象是 obj2,obj2 的原型对象是 obj3 // 因此 obj1 拥有 obj2 和 obj3 的属性 y 和 z,obj2 拥有 obj3 的属性 z obj1.toString(); obj2.toString(); obj3.toString(); // 输出: // Object: obj1, x: 1 // Object: obj1, y: 2 // Object: obj1, z: 3 // Object: obj2, y: 2 // Object: obj2, z: 3 // Object: obj3, z: 3注意:
__proto__不是一个标准属性,但是许多 JS 环境都实现了该属性。从 ES 6 开始,我们可以通过
Object.getPrototypeOf(obj)和Object.setPrototypeOf(obj)来访问一个对象的原型对象,这个等同于非标准实现的对象的__proto__属性。为了避免混淆,本文就一直使用
__proto__来代表一个对象的原型对象。千万注意
__proto__和下文要讲的prototype属性是两个完全不同的属性
被构造函数创建的实例对象的
___proto__指向这个构造函数的prototype属性,例如:function a() { } /** * a 对象(函数也是个对象)的原型现在是这个 { x: 3 } 对象 * 因此 a.x = 3 */ a.__proto__ = { x: 3 }; a.prototype.x = 5; // 将 a 的 prototype 属性对象,添加一个属性 x = 5 /** * 由构造函数(a 对象)生成的对象,它的原型对象等于构造函数对象的 prototype 属性 * 也就是说 b.__proto__ === a.prototype 此时是成立的 * 如果我调用 a.prototype = otherObj,直接修改指针指向的话 * 那么 b.__proto__ === a.prototype 这个等式就不成立了 */ var b = new a(); console.log(a.x); // a.x = a.__proto__.x = 3 /** * b.x = b.__proto__.x = a.prototype.x = 5 * 记住(b.__proto__ === a.prototype) */ console.log(b.x);也可以这么说:只有构造函数对象的
prototype属性才是有意义的,普通对象的prototype属性没有其它的含义(暂时理解是这样),而所有对象(包括构造函数对象)的___proto__属性都指向了这个对象本身的原型对象。
this 引用规则
最外层代码中,this 引用的是全局对象(非模块化的情况下,在 node 中无法还原 this === global,在浏览器环境中有 this === window)
在函数内,this 引用根据 函数调用方式 具有不同的指向:
| 函数的调用方式 | this 引用的引用对象 |
|---|---|
| 构造函数调用 | 构造的新对象 |
| 方法调用 | 接收方对象,例如 obj.method()指向 obj |
| apply 或是 call 调用 | 由 apply 或 call 的参数指定的对象 |
| 其他方式的调用 | 全局对象 |
| 触发事件时的回调函数 | 谁触发的事件,this 就指向谁 |
接收方对象的示例:
var obj = {
x: 100,
doit: function () {
console.log('method is called.' + this.x);
var innerObj = {
x: 20,
inner: function () {
console.log('inner method is called.' + this.x);
}
}
innerObj.inner();
}
}
obj.doit();
// 输出:
// method is called.100
// inner method is called.20
进一步说明接收方的概念(以下示例是跑在 node 环境中,所以全局对象是 global):
global.x = 3; // 给全局对象添加属性 x = 3
var obj = {
x: 100,
doit: function () {
console.log('method is called.' + this.x);
}
}
obj.doit(); // 接收方是 obj,所以输出的是 obj.x = 100
var tmp = obj.doit;
tmp(); // 没有接收方,this 指向全局对象 global,所以输出的是 global.x = 3;
也就是说形如 obj.method() 的调用方式,接收方就是 obj
apply 与 call
通过 apply 与 call 调用的函数的 this 引用可以指向任意对象。也就是说,它们可以显式地指定接收对象,例如:
function f() {
console.log(this.x);
}
var obj = { x: 4 };
f.apply(obj); // 指定此次调用中,f指向的接收对象是 obj,因此输出 obj.x = 4
f.call(obj); // 指定此次调用中,f指向的接收对象是 obj,因此输出 obj.x = 4
var obj = {
x: 3,
doit: function () {
console.log('method is called.' + this.x);
}
};
var obj2 = { x: 5 };
obj.doit.apply(obj2); // 指定此次调用中,f指向的接收对象是 obj2,因此输出 obj2.x = 5
两种方法的第一个参数都是接收方对象,两种方式的区别仅仅在于后续传递参数的方式,例如:
function f(a, b) {
console.log('this.x = ' + this.x + ', a = ' + a + ', b = ' + b);
}
f.apply({ x: 4 }, [1, 2]); // apply 后续参数需要使用数组传递 [1,2]
f.call({ x: 4 }, 1, 2); // call 后续参数按照原来参数的顺序进行传递 1,2
NEW 运算符
MDN 文档:NEW 运算符
new 运算符会进行如下操作:
- 创建一个空的简单
JavaScript对象,也就是{},假设起一个临时名字是tmp - 将新对象
tmp的原型对象指向构造函数(假设是Func()函数)的prototype属性,也就是说tmp.__proto__ = Func.prototype - 此时
tmp.constructor = Func.prototype.constructor = Func - 步骤 3 有两个关键,一个是
tmp.constructor = Func,一个是Func.prototype.constructor = Func- 也就是说新对象的
constructor是从原型链继承来的 - 函数对象的
prototype属性上,有一个属性prototype.constructor指向函数对象自身
- 也就是说新对象的
- 调用新对象的
constructor方法,也就是tmp.constructor(...) - 步骤 5 的接收对象是
tmp,因此构造函数中this指向tmp - 如果
tmp.constructor(...)没有返回新对象,则返回this,也就是tmp对象 new Func等同于new Func(),也就是进行没有任何参数的构造函数调用
示例:
function a(arg) { this.x = 1; console.log(arg); }
var b = new a(); // 输出 undefined
/**
* b.constructor = b.__proto__.constructor 且 b.__proto__ = a.prototype
* 因此 b.__proto__.constructor = a.prototype.constructor
* 而 a.prototype.constructor = a 自身
* 所以 b.constructor = a
* 这里输出 [Function: a]
*/
console.log(b.constructor);
b.__proto__ = Object.prototype;
/**
* 此时 b.constructor = b.__proto__.constructor = Object.prototype.constructor
* 因此 b.constructor = Object
* 所以输出 [Function: Object]
*/
console.log(b.constructor);
a.prototype.constructor(11111); // 等价于 a(11111)调用,因此输出 11111
元属性(属性描述符)
从 ES 5 开始,所有属性都有属性描述符,参考下面的代码:
var myObject = { a: 2 };
console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
它的输出是:
{ value: 2, writable: true, enumerable: true, configurable: true }
这就是 myObject.a 这个属性的描述符,也就是描述这个属性的属性,我将它称为元属性。(所谓元属性就是描述一个属性本身应该有哪些属性,例如我们的数据库,数据库里存的记录是数据,而描述这些记录的记录是元数据,例如数据库表的定义,描述一个表应该有哪些字段,字段应该是什么类型,这就是元数据)
在创建普通属性时,属性描述符会使用默认值,我们也可以用 Object.defineProperty(..) 来添加一个新属性或者修改已有属性(如果它是 configurable),例如:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
console.log(myObject.a); // 2
在 ES 标准中,属性有 2 种类型,一种是数据属性,一种是访问器属性
- 数据属性的属性描述符包含有 4 种属性,分别是 value、writable、configurable、enumerable
- 访问器属性的属性描述符包含有 4 种属性,分别是 configurable、enumerable、get、set
数据属性
Writable
Writable 决定是否可以修改属性的值,例如:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true, enumerable: true
});
myObject.a = 3; // 这里修改 myObject.a 将会失败
console.log(myObject.a); // 2
在严格模式("use strict";)下,甚至会报错。说白了这个元属性类似于 java 的 final 修饰符
默认值:true
Configurable
Configurable 决定我们是否可以用 defineProperty(..) 方法来修改元属性,例如:
var myObject = { a: 2 };
myObject.a = 3;
console.log(myObject.a); // 3
Object.defineProperty(myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true
});
console.log(myObject.a); // 4
myObject.a = 5;
console.log(myObject.a); // 5
Object.defineProperty(myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
}); // TypeError
不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。这个元属性决定了我们是否能继续修改这些元属性,所以一旦为 false,就永远不能撤销了
除了无法修改元属性之外,这个属性本身也无法被删除了,例如:
var myObject = { a: 2 };
console.log(myObject.a); // 2
delete myObject.a;
console.log(myObject.a); // undefined
Object.defineProperty(myObject, "a", {
value: 5,
writable: true,
configurable: false,
enumerable: true
});
console.log(myObject.a); // 5
delete myObject.a; // 静默失败
console.log(myObject.a); // 5
默认值:true
Enumerable
这个描述符控制的是属性是否会出现在对象的属性枚举中, 比如说 for..in 循环。 如果把 enumerable 设置成 false , 这个属性就不会出现在枚举中。
默认值:true
Value
包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候, 把新值保存在这个位置。
从上文可以看出,这个元属性代表了属性的值。
默认值:undefined
访问器属性
访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。
configurable
同数据属性的 configurable 元属性
enumerable
同数据属性的 enumerable 元属性
get (ES 6 之后的语法)
在读取数据值时会被调用的函数,类似于 java 里的 getter 方法,只不过 JS 里定义 get 之后,我们可以直接用属性名称来代替 get 方法的调用,例如:
var obj = {
log: ['a', 'b', 'c'],
get latest() {
if (this.log.length == 0) {
return undefined;
}
return this.log[this.log.length - 1];
}
}
console.log(obj.latest);
// expected output: "c"
如果在 ES 6 之前的话,我们只能使用 Object.defineProperty 来定义访问器属性
访问器属性 get 注意以下问题
- 可以使用数值或字符串作为标识,也就是例如 latest 方法,可以改成
get 1(),然后使用obj[1]进行访问这个属性 - 必须不带参数
- 相同属性不能定义多个 get 方法,而且如果该属性有 value 了,不能定义 get 方法
- 当只是指定了 get,没有指定 set 的时候,表示这个值是只写的,尝试写入会静默失败,在严格模式下会报错
作用和技巧:
当属性计算比较复杂时,用到的时候才计算这个属性
当属性计算很耗费资源的时候,我们可以先定义 get 方法,使用一次去获取属性值之后,再将其删除,然后添加成数据属性,例如:
get notifier() { delete this.notifier; return this.notifier = document.getElementById('bookmarked-notification-anchor'); }, // 上面先是定义成 get,然后再方法里就将其定义成了数据属性,这样可以延迟计算
set
当尝试设置属性时,set语法将对象属性绑定到要调用的函数,例如:
var language = {
set current(name) {
this.log.push(name);
},
log: []
}
language.current = 'EN';
language.current = 'FA';
console.log(language.log);
// expected output: Array ["EN", "FA"]
说白了还是类似于 java 的 get/set 机制。明显来说,当我们定义了一对 get/set 的时候,将相当于定义了一个伪属性,该属性可以读写,跟数据属性很相似。
使用 set 时需要注意以下问题:
- 可以使用数值或字符串作为标识,同上方列举的 get 的注意点的第一条
- 它必须有一个明确的参数,实际上就是我们赋值的那个值会作为参数
- 不能为一个已有真实值的变量使用 set,也不能为同一个属性设置多个 set(稍微思考下就知道这个是必然的)