理解原型对象
创建一个函数,就会根据一组规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
console.log(Person.prototype); // Person { name: 'Zhang san' }
console.log(Person.prototype.constructor === Person); // true
const person = new Person();
console.log(person.__proto__); // Person { name: 'prototype' }
在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性指向构造函数。自定义构造函数的原型对象默认只会获得 constructor 属性,其他的方法和属性都是从 Object 继承而来。
当调用构造函数创建一个实例时,实例内部会包含一个 [[Prototype]]
属性,指向构造函数的原型对象,在浏览器中以 __proto__
表示。
[image:48EDB027-8752-4216-94D9-8AC4BA23B791-30221-00025A8850310CC5/WechatIMG245.png]
对象和原型之间的关系可以通过 isPrototypeOf() 方法来检测。
console.log(Person.prototype.isPrototypeOf(person)); // true
对象有两种方式可以获取到它的原型。
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
console.log(person.__proto__ === Person.prototype); // true, 官方不推荐使用这种方式
当读取一个对象的属性或方法时,会先在对象实例上搜索,如果实例具有给定名称的属性,则返回该属性值。如果没有找到,则继续从原型中搜索。
虽然实例可以访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那么该实例的属性会屏蔽原型中的对应的属性。
function Person() {
}
Person.prototype.name = 'Zhang san';
let person1 = new Person();
let person2 = new Person();
person2.name = 'Li si';
console.log(person1.name); // Zhang san
console.log(person2.name); // Li si
person2 重写 name 属性后原型的 name 属性值被覆盖。person1 的 name 属性不受影响。
通过 delete 操作符能够使得 person2 重新获得原型上的值。
delete person2.name;
console.log(person2.name); // Zhang san
使用对象的 hasOwnProperty() 方法检测实例中的属性。
function Person() {
}
Person.prototype.name = 'Zhang san';
let person1 = new Person();
let person2 = new Person();
person2.name = 'Li si';
console.log(person1.hasOwnProperty('name')); // false
console.log(person2.hasOwnProperty('name')); // true
person2 因为重写了 name 属性,所以返回 true;而 person1 没有该实例属性,所以返回 false.
原型与 in 操作符
有两种方式使用 in 操作符:
- 单独使用
- 在 for-in 循环中使用
单独使用 in 操作符时,用于检测对象能够访问的属性,不管是在实例中还是原型中。
console.log('name' in person1); // true
console.log('name' in person2); // true
前面的例子中 person1 的 name 属性在原型上,person2 的 name 属性在实例上,两者都返回 true。
使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中包括在实例中和原型中的属性。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
Object.defineProperty(Person.prototype, 'age', {enumrable: false, value: 18})
let person2 = new Person();
person2.job = 'Engineer';
for (const prop in person2) {
console.log(prop); // job, name, sayName
}
job 存在于对象中,name 和 sayName() 存在于对象原型,都被正常的枚举,而
age 被定义为不可枚举,所以没有返回。
既然 in 操作符可以检测对象能够访问的属性,而前面讲到 hasOwnProperty() 只能返回存在于对象实例上的属性,那我们可以自定义方法来检测一个属性是否存在于原型上。
function hasPrototypeProperty(object, name) {
return !Object.hasOwnProperty(name) && (name in object);
}
在 ES5 中新增了 Object.keys() 方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
Object.defineProperty(Person.prototype, 'age', {enumrable: false, value: 18})
let person2 = new Person();
person2.job = 'Engineer';
person2.sayName2 = function() {
console.log(this.name);
}
const pKeys = Object.keys(person2);
console.log(pKeys); // ['job', 'sayName2']
我们可以发现 Object.keys() 方法只返回包含在对象上的属性和方法,不包含原型上的,并且不可枚举的属性也不会被返回。
如果想要得到实例的所有属性,无论是否可枚举,可以使用 Object.getOwnPropertyNames() 方法。
const keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // [ 'constructor', 'name', 'sayName', 'age' ]
原型的动态性
在原型中访问属性值其实是一次搜索的过程,因此我们在原型对象上所做的任何修改都能立即从实例上反映出来,即使先创建实例后修改原型。
const friend = new Person();
Person.prototype.sayHi = function() {
console.log('hi');
}
friend.sayHi(); // hi
因为实例与原型之间的连接只是一个引用,而非是一个副本,因此实例对象可以在原型中找到新添加的 sayHi 属性。
但是如果重写整个对象的原型,情况就不一样了。使用构造函数 new 一个对象,会为该实例添加一个指向原型的指针,而把原型修改为另一个对象就会切断构造函数与最初原型之间的联系。
function Person() {
}
const friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Li si',
sayName: function() {
console.log(this.name);
}
}
friend.sayName(); // error
原生对象的原型
原生对象(Object, Array, String, …)的方法都是在其构造函数的原型上定义的。比如 Array 的 sort() 方法。
console.log(Array.prototype.sort); // [Function: sort]
因此我们也可以使用这种方式为原生对象添加自定义的方法。下面的代码为 String 添加一个名为 startWith() 的方法。
String.prototype.startWith = function(text) {
return this.indexOf(text) === 0;
}
const msg = 'Hello world';
console.log(msg.startWith('Hello')); // true
为 String.prototype 添加属性,当前环境下的所有字符串都可以调用。这么做会有风险,如果其他地方添加了相同名称的属性,就会造成命名冲突。
原型对象的问题
原型对象实现了实例之间属性的共享,但也存在一个明显的缺点。共享的属性如果是一个引用类型的对象,在修改的时候会导致所有实例都受影响。
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'Li si',
friends: ['Wang wu', 'Zhang san'],
}
const person1 = new Person();
const person2 = new Person();
person1.friends.push('Li li');
console.log(person1.friends); // [ 'Wang wu', 'Zhang san', 'Li li' ]
console.log(person2.friends); // [ 'Wang wu', 'Zhang san', 'Li li' ]
Person.prototype 上定义的 friends 属性,在修改 person1 实例时, person2 也被修改了。
所以,原型对象一般只用来添加方法,而对象属性直接添加在实例上。
用原型创建一个实例
前面讲到通过构造函数 new 一个实例,该实例的 [[prototype]]
属性指向构造函数的原型。还有一种方法是直接以一个对象原型来创建实例。
const person = {
name: 'Zhang San',
sayName: function() {
console.log(this.name);
}
};
const me = Object.create(person);
me.sayName(); // Zhang san
我们以 person 为原型创建了 me 实例,没有显示的用到构造函数。实际上内部是创建了一个 Object 实例,并将其 [[prototype]]
指针指向了 person。
总结
- 如何检测对象是否存在某个属性;
- 如何遍历对象实例和原型中的属性;
- 如何检测是属性在对象中还是原型中;
- 如何为原生对象添加自定义方法;
- 如何以一个对象为原型,创建实例对象。
本文内容多数为《JavaScript 高级程序设计》阅读笔记。