点击查看【music163】
8. 原型 - 图1

对象的原型(隐式原型)

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个属性可以称之为对象的原型(隐式原型),并且该特殊的属性指向另外一个对象。

  • 注意:双框号包裹代变原型的名称为 prototype,而不是说属性名就是 prototype,不可以obj.prototype调用

早期的ECMA是没有规范如何去查看[[prototype]]的,但是有些浏览器给对象中提供了一个属性**__proto__**, 可以让我们查看一下这个原型对象。node 中也提供了这个属性。
es5 之后开始提供了一个方法可以获取原型:Object.getPrototypeOf()

  1. let obj = { name: 'ls'}
  2. let info = { name: 'zs'}
  3. console.log(obj.__proto__); // [Object: null prototype] {}
  4. console.log(Object.getPrototypeOf(info)); // [Object: null prototype] {}
  5. // 也就是实际的对象是这样:{ name: 'ls', __proto__: {} }

原型有什么用呢?

当我们从一个对象中获取某一个属性时, 它会触发 [[get]] 操作

  1. 在当前对象中去查找对应的属性, 如果找到就直接使用
  2. 如果没有找到, 那么会沿着它的原型链去查找 [[prototype]]

所以我们可以给原型添加属性,而且对象可以访问到该属性。

  1. let obj = { name: 'ls'}
  2. console.log(obj.age); // 获取不存在的属性 age,undefined
  3. obj.age = 18 // 直接在对象上增加属性
  4. console.log(obj.age); // 18 正常获取
  5. Object.getPrototypeOf(obj).gender = 'man' // 在原型上定义 gender 属性
  6. console.log(obj.gender) // man,正常获取

函数的原型(显式原型)

函数也是对象,所以也拥有隐式原型__proto__,但函数本身作为函数对象还会多出一个原型。函数存在一个属性prototypeprototype就是属性名,函数可以通过这个属性直接访问多出来的原型。所以这个原型也叫显式原型。

  1. function foo() { }
  2. console.log(foo.__proto__); // {} 隐式原型
  3. console.log(foo.prototype); // {} 显示原型

new 对显式原型的操作

new 构造函数的时候

  1. 在内存中创建一个新的对象(空对象);
  2. 会将构造函数的显式原型prototype赋值给在内存中这个空对象的隐式原型 [[prototype]]
    • this.__proto__ = Foo.prototype

image.png
也就是说,构造函数后续创建的所有对象的**__protop__**属性都指向构造函数的函数原型。

  1. function Person() { }
  2. let p1 = new Person()
  3. let p2 = new Person()
  4. console.log(p1.__proto__ === Person.prototype) // true
  5. console.log(p2.__proto__ === Person.prototype) // true

因为对象获取一个属性如果从对象自身找不到就会去对象的原型中找,而此时所有的对象原型其实都是函数原型,所以定义在函数原型中的属性将会被所有对象所共有。

  1. function Person() { }
  2. let p1 = new Person()
  3. let p2 = new Person()
  4. // 利用 p1 给函数原型添加 name 属性
  5. p1.__proto__.name = 'zs'
  6. // p2 可以访问到 p1 定义的 name 属性
  7. console.log(p2.name); // zs
  8. // 直接打印函数原型对象查看
  9. console.log(Person.prototype); // { name: 'zs' }

构造函数的终极形态

同样的我们将原先直接定义在构造函数属性上的方法:this.dance = function() {}改成定义在函数原型上:Foo.prototype.dance = function() {},这样就避免了重复创建函数对象的问题。

  1. function Person(name, age) {
  2. this.name = name
  3. this.age = age
  4. // 函数定义在显示原型上
  5. Person.prototype.dance = function() {
  6. console.log(this.name + " It's time to dance");
  7. }
  8. }
  9. let p1 = new Person()
  10. let p2 = new Person()
  11. // p1 p2 两个对象共用了一个函数对象
  12. console.log(p1.dance === p2.dance); // true

constructor 属性

无论显式还是隐式原型对象中都有个constructor属性,该属性指向函数对象,也就是原型对象和函数对象互相指向。

上面这句话乍看不好理解。我们先来总结一下隐式原型和显示原型:

  1. 所有的对象都由构造函数生成。

我们知道生成对象有两种方式 new 和 字面量。new 不必说肯定是构造函数生成。字面量呢,它其实是一个语法糖,实际也是 new 构造函数,只不过 new 的构造函数是 Object()。所以所有的对象都是 new 构造函数生成的。

  1. 所有对象的原型对象**__protop__**必定指向构造函数的原型对象**prototype**

因为第一点指出所有的对象都是 new 出来的,所以某种程度可以说隐式原型对象__protop__这玩意压根不存在,它始终是构造函数的原型对象,区别无非是,是自己写的构造函数的原型对象还是 Object 函数的原型对象。

所以上面这句话应该是这么说:
原型对象中有个**constructor**属性,该属性指向函数对象。也就是函数对象的prototype 属性指向原型对象,原型对象中的 constructor 属性又指向函数对象,函数对象和原型对象互相指向。

为什么控制台打印函数原型对象,显示是个空对象?因为constructor属性的属性描述符中enumerable为 false,不可枚举。所以控制台打印不出来,但可以直接访问。

这也提醒了一点:遍历打印出来对象为空,不一定代变为空,可能是对象的属性不可枚举。查看对象是否为空,通过Object.getOwnPropertyDescriptors()获取对象所有属性的属性描述符才稳妥。

  1. function Person(name, age) {
  2. this.name = name
  3. this.age = age
  4. this.dance = function() { // 会重复生成函数对象
  5. console.log(this.name + " It's time to dance");
  6. }
  7. }
  8. // 显式原型对象
  9. console.log(Person.prototype.constructor); // [Function: Person]
  10. console.log(Person.prototype.constructor === Person); // true 两个相互指向
  11. console.log(Object.getOwnPropertyDescriptors(Person.prototype));
  12. // {
  13. // constructor: {
  14. // value: [Function: Person],
  15. // writable: true,
  16. // enumerable: false, // 不可枚举
  17. // configurable: true
  18. // }
  19. // }
  20. // 隐式原型
  21. let hhh = {}
  22. console.log(Object.getPrototypeOf(hhh).constructor); // [Function: Object]
  23. console.log(Object.getPrototypeOf(hhh).constructor === hhh); // false 因为字面量对象是没有类型的,类型都是Object
  24. console.log(Object.getPrototypeOf(hhh).constructor === Object); // true 所以它是和Object互相指向
  25. console.log(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(hhh)));
  26. // {
  27. // constructor: {
  28. // value: [Function: Object],
  29. // writable: true,
  30. // enumerable: false,
  31. // configurable: true
  32. // },
  33. // // 还有很多其他的属性,其实都是 Object 对象的属性,也就是所有对象都能使用的属性
  34. // ...
  35. // }

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:

  1. function Person(name, age) {
  2. this.name = name
  3. this.age = age
  4. }
  5. Person.prototype = {
  6. dance: function () {
  7. console.log(this.name + " It's time to dance")
  8. },
  9. sing: function () {
  10. console.log(this.name + ' is singing')
  11. }
  12. }

有个问题很容易忽略:本来函数原型对象和函数对象相互指向,但是我们这里相当于给 prototype 重新赋值了一个对象, 那么这个新对象的 constructor 属性, 会指向 Object 构造函数, 而不是 Person 构造函数了。

所以我们需要手动将 constructor 属性指回 Perosn 函数对象。

  1. Person.prototype = {
  2. constructor: Person, // 将constructor 指回函数对象 Perosn
  3. dance: function () {
  4. console.log(this.name + " It's time to dance")
  5. },
  6. sing: function () {
  7. console.log(this.name + ' is singing')
  8. }
  9. }

但是上面这种方式不够完美,因为constructor属性本身是不可枚举的,这样的定义方式,默认是可枚举的,所以最好使用Object.defineProperty()

  1. function Person(name, age) {
  2. this.name = name
  3. this.age = age
  4. }
  5. Person.prototype = {
  6. dance: function () {
  7. console.log(this.name + " It's time to dance")
  8. },
  9. sing: function () {
  10. console.log(this.name + ' is singing')
  11. }
  12. }
  13. // 定义 constructor 属性并指回 Perosn 函数对象
  14. Object.defineProperty(Person.prototype, 'constructor', {
  15. configurable: true,
  16. enumerable: false,
  17. writable: true,
  18. value: Person
  19. })
  20. console.log(Person.prototype.constructor === Person); // true