前些天偶然看到了一个有趣的原型语法,这种方法稍微简化了咱们给原型对象添加方法和属性的书写过程,而且非常清新,给人一种一目了然的感觉,在这里欣喜地和大家分享一下
先来看看我们传统的添加原型对象的属性、方法的方式:
function Person() {
}
Person.prototype.name = 'Han';
Person.prototype.age = 21;
Person.prototype.job = 'student';
Person.prototype.showName = function () {
console.log(this.name);
};
这代码本身没什么问题,但是一连串的 prototype
不免让人感觉亢长拥挤,那有什么办法可以让我们的代码更简洁易读点呢?
其实很简单,我们用一个包含所有属性和方法的对象字面量来书写这个原型对象,可以有更好的辨别度:
function Person() {
}
Person.prototype = {
name: 'han',
age: 21,
job: 'student',
showName: function () {
console.log(this.name);
}
};
是不是感觉清新多了,代码量减少的同时,条理也更加清晰,但这种方式有什么问题吗?
答案是有的,这种方式的确有问题,最明显的,是这种方式重写了原型对象,使得在此之前通过 Person
构造函数创建的实例对象,和原型对象之间的联系断开了。产生的后果是:之前创建的实例,无法共享到此时原型对象上的属性和方法
如:
function Person() {
}
// 实例化
let person = new Person();
// 都显示为 undefined
console.log(person.name, person.age, person.job)
// 报错
person.showName();
Person.prototype = {
name: 'han',
age: 21,
job: 'student',
showName: function () {
console.log(this.name);
}
};
实例对象的 __proto__
指向构造函数的 prototype
,即原型对象,这是他实例化出来的同时自动产生的,他其实就是一个指针,指向一个地址。
后续给原型对象重新赋值,相当于改变了原型对象的地址指向。但是实例化出来的对象的 __proto__
的指向并不会更改,所以导致在重写原型对象之前创建的实例化对象,因为地址指向的不同,无法获取到重写后的属性和方法
简单的表示以下这个过程:
// 实例化对象
let person = new Person();
person.__proto__ === Person.prototype; // true
// 重写这个原型对象,地址发生改变
Person.prototype = {};
person.__proto__ === Person.prototype; // false
所以,这个简写的原型语法,如果想确保每个实例化对象都可以共享到它里面的属性和方法的话,就需要把实例化的过程写在重写原型对象之后
那除了这个问题之外,还有什么其他的问题吗?
的确还有,而且还挺严肃的,咱们看一下:
console.log(person.constructor === Person); // false
console.log(person.constructor); // undefined
等等,我的构造函数不是 Person
吗?怎么没有了?
这的确是一个让人意外的事情,但是仔细一想,一切好像是理所当然。constructor
存放在哪?熟悉原型链的同学都知道,当然是在原型对象中。我们的这种写法是对原型对象的重写,而重写的属性中并没有 constructor
, 所以 constructor
为 undefined
是理所当然
既然我们对原型对象重写了,那它缺少什么咱们补上来就可以了呗:
Person.prototype = {
constructor: Person
};
这下应该没问题了吧:
console.log(person.constructor); // Person
不错,问题好像解决了。。。
等等,好像??这么说还有?看看以下代码:
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'Han',
age: 21
};
let person = new Person();
for (let key in person) {
console.log(key + ':' + person[key]);
}
// 返回结果如下:
// constructor: f Person() {}
// name: han
// age: 21
没错,问题就是使用 for in
循环遍历所有可访问、可枚举的属性时,constructor
也被遍历出来了。可是正常情况下是不会被遍历出来的
这是因为,我们这样直接在原型对象上定义的属性,他们的 数据属性 中的 [[Enumerable]]
属性的值会默认为 true
。它用来控制对应属性是否可以通过 for-in
遍历循环返回出来
这里可能很多人都懵了,这个是个什么东西?
在 ECMAScript 中,定义了一些内部才用的特性,描述了属性的各种特征。它分为两种:数据属性 和 访问器属性。咱们上面说的属于数据属性,它有4个描述其行为的特性:
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性[[Enumerable]]
:表示能否通过for-in
循环返回对应属性[[Writable]]
:表示能否修改属性的值[[Value]]
:包含这个属性的数据值
以上是对数据属性的简单概括,感兴趣的同学可以专门去了解一下,这里仅用于小小的说明
所以,它 [[Enumerable]]
设置为 false
就可以使其无法被遍历到了 ,从而解决问题。而设置这个特殊的属性,需要用到特殊的方法:Object.defineProperty()
,这个方法接收三个参数:
- 属性所在对象
- 属性的名字
- 一个描述符对象
最后咱们可以写成:
function Person() {
}
Person.prototype = {
name: 'Han',
age: 21
};
// 设置 constructor: Person 的同时,屏蔽掉他的可枚举属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
这样,通过 for-in
遍历,也无法返回出 constructor
属性了,这样全部问题得以解决。
可以看的出来,新的原型写法,虽然简单明了,但是问题多多,需要我们手动的解决其本身存在的 bug。但好在修复问题的过程也并不复杂,需要注意的问题也只是:把实例化对象的过程,放在重写原型对象之后。
所以,当需要在原型对象中添加较多的属性和方法时,我们需要更加清晰的看到各个结构,这种方法在此时是一种不错的选择。
OK~ 这就是今天的全部内容了,有什么问题欢迎大家留言建议哦!
今天是2018年的第一天,祝大家在新的一年里顺顺利利!!