理解原型
- constructor —— 构造函数
- prototype —— 指向 原型对象 的属性
- __proto__ —— 指向 实例内部
[[Prototype]]特性 的属性
只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型对象)。 注意是函数,不是实例。
function Person() {}
Person.prototype.name = 'echo';
Person.prototype.sayName = function() {
console.log(this.name);
}
const person = new Person();
console.log(person.prototype); // undefined
console.log(Person.prototype);
// {
// name: "echo"
// sayName: ƒ ()
// constructor: ƒ Person()
// [[Prototype]]: Object
// }
默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。
Person.prototype指向原型对象,而Person.prototype.constructor指回Person构造函数
console.log(Person.prototype.constructor); // ƒ Person() {}
每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。
脚本中没有访问这个[[Prototype]]特性的标准方式,但Firefox、Safari、Chrome会在每个对象上暴露 __proto__属性,通过这个属性可以访问对象的原型。(在其他实现中,这个特性完全被隐藏了)。
console.log(person1.__proto__);
// {
// name: "echo"
// sayName: ƒ ()
// constructor: ƒ Person()
// [[Prototype]]: Object
// }
那么,接下来,让我们再看看他们之间的关系:
// 构造函数通过prototype属性链接到原型对象
Person.prototype.constructor === Person
// 实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
person1.__proto__ === Person.prototype
// 实例与构造函数没有直接联系,与原型对象有直接联系
person1.__proto__.constructor === Person
// 正常的原型链都会终止于Object的原型对象
Person.prototype.__proto__.constructor === Object
// 现在试着理解 通过对象的__proto__属性可以访问它自己的原型 这句话
Person.prototype.__proto__ === Object.prototype
// Object原型的原型是null
Person.prototype.__proto__.__proto__ === null
同一个构造函数创建的两个实例共享同一个原型对象
person1.__proto__ === person2.__proto__
// 通过Instanceof检查实例的原型链中是否包含指定构造函数的原型
person1 instanceof Person; // true
person1 instanceof Object; // true
Person.prototype instance Object; // true
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有,简言之,构造函数,原型对象和实例是3个完全不同的对象
虽然不是所有实现都对外暴露了[[Prototype]],但可以使用isPrototypeOf()方法确定两个对象之间的这种关系。本质上,isPrototypeof()会在传入参数的[[Prototype]]指向调用它的对象时返回true。
console.log(Person.prototype.isPrototypeof(person1)); // true
ECMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
也就是说,使用Object.getPrototypeOf()可以方便地取得一个对象的原型。
console.log(Object.getPrototypeOf(person1).name);
Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值,也就是 重写一个对象的原型继承关系 的意思。
const a = {
age: 12
}
const person = {
name: 'echo'
}
Object.setPrototypeOf(person, a);
console.log(person.name); // 'echo'
console.log(person.age); // 12
console.log(Object.getPrototypeOf(person) === a); // true
警告:Object.setPrototypeOf()可能会严重影响代码性能,这种影响并不仅是执行Object.setPrototypeOf()语句那么简单,而是会涉及所有访问了那些修改过[[Prototype]]的对象的代码
所以代码中常见的方式是通过Object.creat()来创建一个新对象,同时为其指定原型
const a = {
age: 12
}
const person = Object.create(a);
person.name = 'echo';
console.log(person.name); // 'echo'
console.log(person.age); // 12
console.log(Object.getPrototypeOf(person) === a); // true
理解原型链
总结一下构造函数、原型和实例的关系:
-
每个构造函数都有一个原型对象
-
原型有一个属性指回构造函数
-
而实例有一个内部指针指向原型
在通过对象访问属性时,会按照这个属性的名称开始搜索。
搜索开始于对象实例本身,如果没找到这个属性,则会沿着指针进入原型对象进行搜索。
比如在调用person1.sayName()时,会发生两步搜索:
- JS引擎在
person1实例上搜索sayName属性,未果,开始向上查找 person1实例的原型上有sayName属性,于是会返回保存在原型上的这个函数
最坏的结果是查找到最外层的Object,仍未找到,则会返回undefined,查找结束
这就是原型用于在多个对象实例间共享属性和方法的原理
虽然可以通过实例读取原型对象上的值,但不会通过实例重写这些值。在实例上设置同名属性,只是会让查找提前返回实例上对应的值(包括将属性值设置为null),而不会向上查找而已。
使用delete操作符可以完全删除实例上的这个属性,从而让查找过程上升到原型对象
ECMAScript的Object.getOwnPropertyDescriptor()方法只对实例属性有效。要取得原型属性的描述符,必须直接在原型对象上调用该方法
in操作符会在可以通过对象访问指定属性时返回true,包括实例和原型。
in操作符的另一种用法——for-in,用于遍历对象中(包括实例和原型链,可枚举和不可枚举属)的属性
而hasOwnProperty()方法用于确定某个属性是不是属于实例自身。
function Person() {}
Person.prototype.name = 'echo';
let person1 = new Person();
console.log(person1.hasOwnProperty('name')); // false
console.log('name' in person1); // true
person1.name = 'Jon';
console.log(person1.hasOwnProperty('name')); // true
console.log('name' in person1); // true
修改原型的其他写法:
为了减少代码冗余,从视觉上更好地封装原型功能,使用以下方法来对原型进行重写。
function Person() {}
Person.prototype = {
name: 'echo',
age: 12,
sayName() {
console.log(this.name);
}
}
那么,问题来了...
在以上代码中,Person.prototype等于被设置为一个新对象,这样重写之后,Person.prototype的constructor属性就不指向Person了。
在创建函数时,也会创建它的prototype对象,同时会自动给这个原型的constructor属性赋值。
而上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了完全不同的新对象(Object构造函数),不再指向原来的构造函数。
此时,虽然instanceof操作符还能可靠地返回值,但已经不能再依靠constructor属性来识别类型了
let person = new Person();
console.log(person instanceof Person); // true
console.log(person.constructor === Person); // false
事实证明,虽然随时能给原型添加属性和方法,并能够立即反应在所有对象实例上,但这跟重写整个原型是两回事。
当然,我们可以在重写原型对象时,专门设置一下它的值,保证这个属性仍然包含恰当的值
...
Person.prototype = {
constructor: Person,
...
}
但是要注意,以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的。可以使用Object.defineProperty()方法来定义constructor属性
...
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
实例只有指向原型的指针,没有指向构造函数的指针。
function Person() {}
let person = new Person(); // 注意实例化Person的位置
Person.prototype = {
constructor: Person,
name: 'echo',
age: 12,
sayName() {
console.log(this.name);
}
}
let person2 = new Person();
person2.sayName(); // 'echo'
person.sayName(); // TypeError: person.sayName is not a function
以上代码,person实例是在重写原型对象之前创建的,在调用person.sayName()时会导致错误。这是因为person指向的原型还是最初的原型,而这个原型上并没有sayName属性。
原生对象的原型
原型模式之所以重要,不仅体现在自定义类型上,还因为它也是实现所有原生引用类型的模式。
所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法。比如,数组实例的sort()方法就是Array.prototype上定义的,字符串包装对象的substring()方法也是String.prototype上定义的
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。
不推荐在生产环境中修改原生对象原型,这样做很可能造成误会,而且可能引发命名冲突,另外还有可能意外重写原生的方法
推荐的做法是创建一个自定义的类,继承原生类型