JavaScript中的原型和原型链

提问

  • 原型是用来干什么的?
  • 为什么原型那么重要?
  • 如何合理的使用原型?

可能是之前开发和思考的层次比较浅,个人感觉多数的开发工作与原型和原型链的关系不大,甚至不知道的情况下也能够把任务完成。网上也有不少讲解原型的文章,更有不少面经中提到这个考题,如果不是为了炫技那必是开发中必须要掌握的一个知识点。最近也看了这方面的资料,就上面的三个问题总结一下自己的想法,其实学以致用才是最棒的,目前开发经验少不能体会原型的好处,只做记录方便之后遇到问题能够知道该去查找哪方面的资料。

prototype、proto

prototype

几乎所有函数都有一个显示的 prototype 属性,除了下面的这种情况

let fun = Function.prototype.bind()
复制代码

prototype 属性是一个对象,在函数声明的时候就会自动创建,而且默认情况下只有一个属性 constructor 用来指向函数本身。

image.png

proto

每个对象都会有的一个隐式原型属性,指向了创建该对象的构造函数的原型。

image.png

__proto__ 是在对象创建的时候添加属性,链接到构造调用的函数的原型上的。当访问对象上不存在的属性的时候可以顺着这条链查找属性。
new 一个对象的过程可以解析为一下4步:

  • 创建一个空对象 obj
  • obj 的 __proto__ 链接到函数的 prototype
  • 执行构造调用的函数,初始化属性
  • 判断函数返回值是否为一个对象类型,是则返回该返回值,否则返回 obj
// new 语法的简单模拟
var creatObject = function(constructor,...args) {
	var obj = Object.create(null)
  // 链接原型
	Object.setPrototypeOf(obj,constructor.prototype)
	res = constructor.apply(obj,args)
	return typeof res === 'object' ? res : obj
}
复制代码

image.png

理解原型

在JavaScript中没有类这个概念,所谓的继承也是通过原型链来实现的,包括加入的 class 语法糖也是为了更好的理解JavaScript中的“继承”并没有改变JavaScript中对象的工作机制,底层还是原型。那么很自然的想到原型链中也有类似继承的好处。
我的理解上原型有两个主要的作用:

  • 用来理解对象上属性访问的过程
  • 用来判断对象实例是否由某个函数创建

对象上属性访问的过程

在对象上查找属性的时候,若对象本身不具备该属性,则被查找的属性会被委托在整个原型链上,只有当没有更多的原型可以查找的时候,才停止查找。
我们定义一个对象 var o = {a:1,b:2} 我们知道可以使用 hasOwnProperty 来判断某属性是否对象自身拥有的属性,但是在创建对象的时候并没有创建 hasOwnProperty 方法,这个是如何通过 . 运算调用的呢?

image.png

我们通过 o.__proto__===Object.prototype 可以知道通过字面量的方式创建对象实例 o 的时候的函数是 Object 且现在 o 的原型链上有 Object.prototype 
image.png

我们再查看一下 Object.prototype 发现这个对象中含有属性 hasOwnProperty 
image.png

也就是说,在 o 中没有找到 hasOwnProperty ,就顺着原型链往上找,首先找 o.__proto__ 也就是 Object.prototype ,很巧在这里就找到了(如果没有找到就会一直顺着这条链往上找),于是拿过来使用。
image.png

判断对象实例是否由某个函数创建

constructor

前面略带的提过这个属性 constructor ,通过这个属性可以访问创建该对象时所用的函数,通过这个我们可以判断两个实例是否是同一个函数创建而来的,我想这个属性的作用差不多也应该是这样了。

image.png

instanceof

obj instanceof Foo 应该被理解为检测函数 Foo 是否出现在 obj 的原型链中,并不是单纯的理解过对象实例是否由某个函数构造调用创建。只要通过 __proto__ 一层一层的往上找能够找到这个函数的 prototype 就说明是这个函数创建的。

image.png

image.png

但是当修改了原型链的时候可能检查不准,现在 dog.__proto__!==Dog.prototype 而且顺着 __proto__ 也找不到 Dog.prototype 所以这里返回 false 
image.png

原型的一些注意点

遮蔽效应

当实例属性和原型属性中有同名属性或者方法的时候优先使用实例属性。下面实例属性中和原型属性中都有 say 函数,这里使用实例属性。顺着原型链查找现在实例属性中找到了就停止查找

image.png

函数原型之间的引用关系

对象与函数原型之间的引用关系是在对象创建的时候建立的,新创建的对象会根据当前的函数原型建立引用。

image.png

继承的正确实现方式

上面例子中用一个对象字面量重写了 Dog.prototype 此时 constructor 属性不见了,这个在实习继承效果的时候也需要注意,我们在修改完原型之后需要补上 constructor

// es5
function Animal() {}
Animal.prototype.say = function() {
  console.log('动物叫')
}
function Dog() {}
let dog = new Dog()
// 链接原型并加上constructor
Dog.prototype = Object.create(Animal.prototype, {
  constructor: {
    value: Dog, // 指向Dog
    enumerable: false,
    writable: true,
    configurable: true
  }
})
//es6 引入了class 和 extends 减少对原型覆盖的副作用
class Animal{
  constructor(name) {
    this.name = name
  }
  say(){
    console.log('动物叫')
  }
}
class Dog extends Animal{
  constructor(name,type){
    super(name) // 调用父类构造函数
    this.type = type
  }
	say(){
    console.log('汪汪')
  }
}
var dog = new Dog('道格','中华田园犬')
dog.say() // 汪汪
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
复制代码

抽离公共方法到函数原型

一些公共方法如果作为实例方法创建在每创建一个实例的时候都会产生一个副本,不仅占内存而且修改起来麻烦。可以在函数的原型对象上创建对象方法,这样可以使得一个方法被所有对象实例共享(可以通过原型链访问)。

// 翻转字符串为例
String.prototype.reversed = function(){
  return Array.from(this).reverse().join('')
}
"abcdef".reversed() // "fedcba"
复制代码

参考资料