原型
如果你需要创建一个对象,但不希望从头创建,而是利用一个已存在的对象创建,只是修改(扩展)其部分属性,这种情况下可以使用原型继承的方式实现:
let person = {
name: 'Jonh',
age: 20
}
let teacher = {
school: 'MIT',
__proto__: person
}
console.log(teacher.name);
console.log(teacher.age);
console.log(teacher.school)
// Jonh
// 20
// MIT
这里的__proto__是一个特殊的访问器属性,可以利用它设置对象的原型属性[[prototype]],当一个对象的原型属性是另一个对象时,访问这个对象的属性时如果发现该属性不存在,就会在它的原型属性中查找。
可以存在多层原型继承,形成一个原型链:
let animal = {
eat() {
console.log('Eat');
}
};
let rabbit = {
jump() {
console.log('Jump');
},
__proto__: animal
}
let longEarRabbit = {
__proto__: rabbit
}
longEarRabbit.eat();
longEarRabbit.jump();
修改属性
如果修改由原型继承的属性:
let animal2 = {
eat() {
console.log('Eat');
}
};
let rabbit2 = {
__proto__: animal2
}
rabbit2.eat = function() {
console.log('Eat2');
}
rabbit2.eat();
animal2.eat();
// Eat2
// Eat
实际上是为当前对象创建一个新属性,并不会影响到原型对象的属性。
访问器属性略有不同,因为访问器属性的读写实际上是方法调用,而非简单的属性赋值:
let person2 = {
firstName: 'Jonh',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set fullName(value) {
let parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
}
let teacher2 = {
school: 'MIT',
__proto__: person2
}
teacher2.fullName = 'Mike Smith';
console.log(teacher2.fullName);
console.log(person2.fullName);
// Mike Smith
// Jonh Doe
这里有个细节,通过访问器属性修改this.firstName与this.lastName的值时,this指向的是访问器属性调用时的对象,因此这里只有teacher2的相应属性被修改,而person2的属性不变。
for...in
通过原型继承的属性,会在 for...in 时被遍历:
let person3 = {
name: 'Jonh',
age: 20,
}
let teacher3 = {
school: 'MIT',
__proto__: person3
}
console.log(Object.keys(teacher3));
// [ 'school' ]
for(let key in teacher3) {
console.log(key);
}
// school
// name
// age
可以用一个hasOwnProperty方法区分哪些属性是自己拥有的,哪些是从原型继承的:
for(let key in teacher3) {
if(teacher3.hasOwnProperty(key)) {
console.log('Own property: ' + key);
}
else {
console.log('Inherited property: ' + key);
}
}
// Own property: school
// Inherited property: name
// Inherited property: age
prototype
let basePerson = {
toString() {
return `${this.name} ${this.age}`;
}
}
let Person = function(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = basePerson;
let person4 = new Person('Jonh', 20);
console.log(person4.toString());
// Jonh 20
console.log(person4.__proto__ === Person.prototype)
// true
在这个示例中,利用new Person创建了一个新对象,不同的是,这里为Person函数添加了一个普通属性prototype,此时新创建的对象person4会自动通过原型继承的方式指向Person函数的prototype属性。
这里的
prototype是一个普通属性,仅用于为新对象设置原型继承,与前面提到的原型继承特殊属性[[prototype]]没有关系。
实际上,函数默认存在一个prototype属性,它有一个constructor属性,指向函数自己:
let Person5 = function(name, age) {
this.name = name;
this.age = age;
}
console.log(Person5.prototype.constructor === Person5);
可以利用这一点很容易地获取到创建对象的构造函数,并创建新对象:
let Person5 = function(name, age) {
this.name = name;
this.age = age;
}
console.log(Person5.prototype.constructor === Person5);
let person5 = new Person5('Jonh', 20);
let person6 = new person5.constructor('Mike', 30);
console.log(person6.name);
// Mike
这存在一些缺陷,比如就像前面展示的,new方法的prototype属性可以随便重写覆盖,如果覆盖后的对象中不包含constructor属性或者该属性并不指向new方法自身,上面的使用方式就没有意义。
因此,如果需要修改new方法的prototype属性,更推荐的方式是不要覆盖,仅对默认属性进行扩展:
let Person7 = function (name, age) {
this.name = name;
this.age = age;
}
Person7.prototype.toString = function () {
return `${this.name} ${this.age}`;
}
let person7 = new Person7('Jonh', 20);
console.log(person7.toString());
或者在覆盖的时候正确设置constructor:
let Person8 = function (name, age) {
this.name = name;
this.age = age;
}
Person8.prototype = {
constructor: Person8,
toString() {
return `${this.name} ${this.age}`;
}
}
let person8 = new Person8('Jonh', 20);
console.log(person8.toString());
原生原型
即使我们创建的是一个空对象,它也可以使用toString方法:
let obj = {};
console.log(obj.toString());
// [object Object]
实际上obj={}相当于obj2 = new Object(),借助前面说到的内容,很容易猜想到toString方法是来自Objeect.prototype指向的对象:
let obj2 = new Object();
console.log(obj2.__proto__ === Object.prototype);
console.log(obj2.toString === obj2.__proto__.toString);
console.log(obj2.toString === Object.prototype.toString);
// true
// true
// true
其他内置原型
除了Object,JS 中还存在其他内置原型:
let arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype);
// true
let func = function () { };
console.log(func.__proto__ === Function.prototype);
// true
这些原型的原型指向Object.prototype。
基本类型
对于基本类型(字符串、number、boolean),它们不是对象,但是依然存在原型,在调用这些类型的方法时,JS 会临时使用原型创建对象,并调用这些原型对象的方法,然后销毁原型对象。
因此String.prototype、Number.prototype和Boolean.prototype依然可以使用。
更改原生原型
可以通过修改原生原型的方式为所有同一类型的变量添加新功能:
String.prototype.repeat = function (times) {
let result = '';
for (let i = 0; i < times; i++) {
result += this;
}
return result;
}
console.log('a'.repeat(5));
// aaaaa
虽然这么做有效,但一般不建议这么做,因为这很容易出现冲突,比如 JS 库 A 为String.prototype添加了一个show方法,JS 库 B 也为String.prototype 添加了一个show 方法,两者就会冲突,最终其中一个会覆盖另一个。
唯一建议这么做的情景是,如果某个 JS 新特性部分浏览器还不支持,可以通过这种方式添加,比如:
if (!String.prototype.repeat) { // if there's no such method
// add it to the prototype
String.prototype.repeat = function(n) {
// repeat the string n times
// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
从原型中借用
可以从已有的原型中借用一些有用的方法,比如:
let obj3 = {
0: 'hello',
1: 'world',
length: 2
}
obj3.join = Array.prototype.join;
console.log(obj3.join(','));
// hello,world
这里利用Array.prototype中的join方法为obj3对象添加了一个join方法,因为数组的join方法本身只要求接收的参数是一个类数组(数字索引 + length 属性)。
当然,如果有必要,可以将obj3的原型设置为Array.prototype,这样obj3就可以使用所有的数组方法。
No __proto__
现代 JS 规范中不推荐使用__prototype__直接获取/修改原型。更现代的方式:
let obj4 = {
0: 'hello',
1: 'world',
length: 2
};
console.log(Object.getPrototypeOf(obj4) === Object.prototype);
Object.setPrototypeOf(obj4, Array.prototype);
console.log(obj4.join(','));
// hello,world
除了getPrototypeOf和setPrototypeOf外,还有一个create方法,可以创建一个使用了指定原型对象的新对象:
let obj5 = Object.create(Array.prototype);
obj5.push('hello', 'world');
console.log(obj5.join(','));
// hello,world
__proto__仅应该在定义对象的时候指定原型时使用:
let obj6 = {
0: 'hello',
1: 'world',
length: 2,
__proto__: Array.prototype
}
console.log(obj6.join(','));
不过同样也可以用create方法取代,因为create方法可以接收一个属性描述符参数:
let obj7 = Object.create(Array.prototype,{
0: {
value: 'hello'
},
1: {
value: 'world'
},
length: {
value: 2
}
});
console.log(obj7.join(','));
The End.
本文的完整示例代码可以从获取。
参考资料

文章评论