面向对象(二):认识JavaScript中对象的原型

317 阅读6分钟

JavaScript 对象系列

面向对象(一):认识对象

面向对象(二):认识JavaScript中对象的原型

面向对象(三):创建多个对象的方案

面向对象(四):掌握原型链

面向对象(五):ES6 类的基本使用

面向对象(六):JavaScript中的7种继承方式

面向对象(七):ES6的class转ES5的源码阅读

第二篇

在上一篇中,面向对象(一):认识对象,了解到了JavaScript中对象存在的意义,以及加深了对象属性的认识。在这一篇中就主要来聊一聊对象的原型。

对象的原型(隐式原型)

JavaScript 当中每个对象都有一个特殊的内置属性 [[prototype]] ,这个特殊的对象指向另外一个对象。

Object.create(null)除外

那么对象原型的作用是什么呢?

当通过对象的属性 key 来获取 value 值的时候,先会查看对象本身是否有对应的 key 值,如果存在,就使用它;如果本身上没有找到,就会去 [[prototype]] 指向的对象上去找,如果找到就返回;

获取的方式

在早期的 ECMAScript 是没有规范如何查看内置的 [[prototype]]

浏览器提供了一个属性(__proto__)可以用来查看内置的原型,但是有兼容性问题(node环境下,好像也支持)。

12_01.png

在 ES5 之后 Object 提供了一个方法(getPrototypeOf),来获取对象的原型。

12_02.png

由于这个属性(__proto__)是浏览器提供了,实际上不是真实存在的,所以它被称为隐式原型

注意:在开发中,不能使用它。

函数的原型(显式原型)

在上面知道了基本上所有的对象都是具有原型的,即__proto__属性(虚拟的)。

  • 函数也是对象,所以函数也是有隐式原型的(__proto__)。
  • 函数当成一个对象时,它具有隐式原型;函数被看成是一个函数时,它有着它独特的原型,被称为显式原型(prototype)

为什么被称为显式原型?

相当于隐式原型的描述属性(__proto__)不是真实存在的,是浏览器提供的一个属性,存在浏览器的兼容性问题。而显式原型的描述符(prototype)是真实存在的,是可以在开发中使用的属性,不存在浏览器的兼容性问题,所以被称为显式原型

12_03.png

隐式原型和显式原型之间的关联

创建对象的两种方式:

  1. 字面量(常用)
  2. 构造函数
 var obj1 = {}  // 字面量
 var obj2 = new Object() // 构造函数

obj1通过字面量创建的,具有隐式原型属性,没话可说。

obj2通过构造函数创建的一个对象,也是具有隐式原型(__proto__)属性的;而Object() 是一个构造函数,构造函数也是一个函数,所有它具有显式原型(prototype)属性。

那么 obj2 对象的隐式原型与 Object 构造函数的显示原型有什么关联呢?

先从构造函数说起。

认识构造函数

构造函数本质上就是一个函数,表现形式跟普通函数一样。所以说,从表现形式上,是没法区分构造函数与普通函数的。那么就从其他方面来区分?函数调用

  1. 如果使用()来进行调用函数,那么它就是普通函数;
  2. 如果使用new关键词来进行调用,那么它就是构造函数;
 // 定义一个函数
 function foo() {}
 ​
 foo() // 普通函数
 ​
 new foo() // 构造函数   如果不传递参数的话,也可以写成 new foo

这下知道了构造函数和普通函数的区别了吧。

在平时的开发过程中,约定俗成的规定,构造函数的首字母大写,目的是为了提醒其他开发人员,该函数希望通过 new 关键词被调用。

new 操作符调用的作用

在上面我们知道,构造函数是通过 new 关键词进行调用的,那么它有何不同?

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象)。
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性。
  3. 构造函数内部的this,会指向创建出来的新对象。
  4. 执行函数的内部代码(函数体代码)。
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象。

这里呢,就不研究 new 内部的具体实现了,这里只需要关注第二步,对象内部的 [[prototype]] ,也就是对象的隐式原型指向构造函数的显式原型(都指向了同一个地址)。

 function Person() {}
 const p = new Person()
 console.log(p.__proto__ === Person.prototype) // true

在内存中的表现形式

12_04.png

这里只是大致画了一下堆内存中的情况,但是也能清晰明了的知道一个结论:

对象的隐式原型与该对象的构造函数的显式原型指向的同一个对象(0x100

操作函数的原型对象

原型的作用:就是实现数据共享

只要在函数的原型对象中,添加一些属性,就可以在创建出来的对象实例中使用(p1p2)。

 function Person() { }
 ​
 Person.prototype.name = 'copyer'
 Person.prototype.age = 18
 ​
 var p1 = new Person()
 ​
 console.log(p1.name) // copyer
 console.log(p1.age)  // 18

12_05.png

注意到没有,在函数的原型对象中始终有一个属性(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
 })

最后内存的表现形式:

12_06.png

总结

梳理了一遍 JavaScript 中的对象的原型,对原型的了解也更加的透彻了,从三个方面:

  • 对象具有隐式原型(__proto__
  • 函数具有显式原型(prototype
  • 隐式原型与显式原型之间的联系

理解上面的三点,打好基础,接下来就是对原型链的学习了。