简单有趣的原型语法

前些天偶然看到了一个有趣的原型语法,这种方法稍微简化了咱们给原型对象添加方法和属性的书写过程,而且非常清新,给人一种一目了然的感觉,在这里欣喜地和大家分享一下

先来看看我们传统的添加原型对象的属性、方法的方式:

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, 所以 constructorundefined 是理所当然

既然我们对原型对象重写了,那它缺少什么咱们补上来就可以了呗:

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年的第一天,祝大家在新的一年里顺顺利利!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值