JavaScript 对象系列:
第二篇
在上一篇中,面向对象(一):认识对象,了解到了JavaScript中对象存在的意义,以及加深了对象属性的认识。在这一篇中就主要来聊一聊对象的原型。
对象的原型(隐式原型)
JavaScript 当中每个对象都有一个特殊的内置属性 [[prototype]] ,这个特殊的对象指向另外一个对象。
Object.create(null)除外
那么对象原型的作用是什么呢?
当通过对象的属性 key 来获取 value 值的时候,先会查看对象本身是否有对应的 key 值,如果存在,就使用它;如果本身上没有找到,就会去 [[prototype]] 指向的对象上去找,如果找到就返回;
获取的方式
在早期的 ECMAScript 是没有规范如何查看内置的 [[prototype]] 。
浏览器提供了一个属性(__proto__)可以用来查看内置的原型,但是有兼容性问题(node环境下,好像也支持)。
在 ES5 之后 Object 提供了一个方法(getPrototypeOf),来获取对象的原型。
由于这个属性(__proto__)是浏览器提供了,实际上不是真实存在的,所以它被称为隐式原型
注意:在开发中,不能使用它。
函数的原型(显式原型)
在上面知道了基本上所有的对象都是具有原型的,即__proto__属性(虚拟的)。
- 函数也是对象,所以函数也是有隐式原型的(
__proto__)。
- 函数当成一个对象时,它具有隐式原型;函数被看成是一个函数时,它有着它独特的原型,被称为显式原型(prototype) 。
为什么被称为显式原型?
相当于隐式原型的描述属性(
__proto__)不是真实存在的,是浏览器提供的一个属性,存在浏览器的兼容性问题。而显式原型的描述符(prototype)是真实存在的,是可以在开发中使用的属性,不存在浏览器的兼容性问题,所以被称为显式原型。
隐式原型和显式原型之间的关联
创建对象的两种方式:
- 字面量(常用)
- 构造函数
var obj1 = {} // 字面量
var obj2 = new Object() // 构造函数
obj1通过字面量创建的,具有隐式原型属性,没话可说。
obj2通过构造函数创建的一个对象,也是具有隐式原型(__proto__)属性的;而Object() 是一个构造函数,构造函数也是一个函数,所有它具有显式原型(prototype)属性。
那么 obj2 对象的隐式原型与 Object 构造函数的显示原型有什么关联呢?
先从构造函数说起。
认识构造函数
构造函数本质上就是一个函数,表现形式跟普通函数一样。所以说,从表现形式上,是没法区分构造函数与普通函数的。那么就从其他方面来区分?函数调用。
- 如果使用
()来进行调用函数,那么它就是普通函数; - 如果使用
new关键词来进行调用,那么它就是构造函数;
// 定义一个函数
function foo() {}
foo() // 普通函数
new foo() // 构造函数 如果不传递参数的话,也可以写成 new foo
这下知道了构造函数和普通函数的区别了吧。
在平时的开发过程中,约定俗成的规定,构造函数的首字母大写,目的是为了提醒其他开发人员,该函数希望通过 new 关键词被调用。
new 操作符调用的作用
在上面我们知道,构造函数是通过 new 关键词进行调用的,那么它有何不同?
如果一个函数被使用new操作符调用了,那么它会执行如下操作:
- 在内存中创建一个新的对象(空对象)。
- 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性。
- 构造函数内部的this,会指向创建出来的新对象。
- 执行函数的内部代码(函数体代码)。
- 如果构造函数没有返回非空对象,则返回创建出来的新对象。
这里呢,就不研究 new 内部的具体实现了,这里只需要关注第二步,对象内部的 [[prototype]] ,也就是对象的隐式原型指向构造函数的显式原型(都指向了同一个地址)。
function Person() {}
const p = new Person()
console.log(p.__proto__ === Person.prototype) // true
在内存中的表现形式
这里只是大致画了一下堆内存中的情况,但是也能清晰明了的知道一个结论:
对象的隐式原型与该对象的构造函数的显式原型指向的同一个对象(0x100) 。
操作函数的原型对象
原型的作用:就是实现数据共享。
只要在函数的原型对象中,添加一些属性,就可以在创建出来的对象实例中使用(p1、p2)。
function Person() { }
Person.prototype.name = 'copyer'
Person.prototype.age = 18
var p1 = new Person()
console.log(p1.name) // copyer
console.log(p1.age) // 18
注意到没有,在函数的原型对象中始终有一个属性(constructor), 它是内置的属性,属性值是一个函数,指向构造函数。
function Person() { }
console.log(Person.prototype) // {}
既然 constructor 属性内置在 prototype的对象中,那么打印为什么还是一个{}呢?
这里就可以大胆的猜测,是constructor的属性描述符中的 enumerable 为false,才不能被遍历。
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'constructor'))
/*
{
value: [Function: Person], // 指向构造函数
writable: true,
enumerable: false, // 不能被遍历
configurable: true
}
*/
果真如此。
重置函数的原型对象
当对函数的原型对象添加新的共享属性的时候
Person.prototype.name = 'copyer'
Person.prototype.age = 18
...
// 如果有很多的话,那么就要写很多遍 Person.prototype.xxx = yyy
上面的情况,如果不嫌麻烦,可以这样写,但是我们也可以写成对象的形式(新的对象)
Person.prototype = {
name: 'copyer',
age: 18,
...
}
这样写,也没有什么毛病。但是唯一不足的地方就是:函数的原型对象中少了一个 constructor 属性,因为这是内置的属性,所以需要我们手动添加上。
Person.prototype = {
constructor: Person, // 这样写好吗?
name: 'copyer',
age: 18,
...
}
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'constructor'))
/*
{
value: [Function: Person], // 指向构造函数
writable: true,
enumerable: true,
configurable: true
}
*/
这里的enumerable变成了true,它是可以被遍历的,跟内置的不符合,所以需要修改。
// 这样写,才合理
Person.prototype = {
name: 'copyer',
age: 18,
...
}
// 单独写 constructor 的属性描述符
Object.defineProperty(Person.prototype, 'constructor', {
value: Person,
writable: true,
enumerable: false,
configurable: true
})
最后内存的表现形式:
总结
梳理了一遍 JavaScript 中的对象的原型,对原型的了解也更加的透彻了,从三个方面:
- 对象具有隐式原型(
__proto__) - 函数具有显式原型(
prototype) - 隐式原型与显式原型之间的联系
理解上面的三点,打好基础,接下来就是对原型链的学习了。