写在前面

我们知道在面向对象编程的语言中,有一句统筹全局的中心句—”万物皆对象“,原型和原型链也是基于这个基础理解的。

对于初学js的继承机制—”原型“和”原型链“这两个概念的理论时,总是忘了记、记了忘。所以死记硬背真的是没得用的,得深入理解其背后的设计思想,得理解加记忆,如虎添翼。

至于为什么这样说,就随着这篇文章去揭开珍妮的面纱,如剥洋葱般去探究它的本质。

来不及解释,快上车。

原型和原型链 - 图1

JS继承的设计思想

我们知道创建对象有两种方式:一种是最常见的对象字面量,一种就是常说的通过new来创建对象实例。其实这两种方式描述的对象都是等价的,属性和方法都是一致的。

  1. // 字面量对象
  2. let obj = {
  3. name:"yichuan",
  4. age:18,
  5. sayName(){
  6. console.log("name: ", this.name);
  7. }
  8. }
  9. // new创建对象实例
  10. let obj2 = new Object();
  11. obj2.name = "pingping";
  12. obj2.sayName = function(){
  13. console.log("name: ", this.name);
  14. }

使用对象字面量或者Object构造函数可以轻松创建对象,但是在创建具有同样接口的对个对象时,会重复编写很多代码。那么,我们想可不可以创建一个容器,将共享的属性和方法存在里面,这样就可以在多个对象中使用。

在es6之前没有正式支持类和继承的结构,但是能够通过原型链继承进行模仿实现类和继承。事实上,es6的类也的确是封装了构造函数和原型继承的语法糖。

工厂模式

工厂模式可以用于抽象创建特定对象过程,解决创建多个类似对象的问题,但是没有解决对象标识的问题,不能设置新创建对象的类型。

  1. // 工厂模式
  2. function createPerson(name, age, city){
  3. let obj = new Object();
  4. obj.name = name;
  5. obj.age = age;
  6. obj.city = city;
  7. obj.sayName = function(){
  8. console.log("my name is : ", this.name);
  9. }
  10. return obj;
  11. }
  12. let preson1 = createPerson("yichuan",18,"BeiJing");
  13. let preson2 = createPerson("onechuan",28,"GuangZhou");

构造函数模式

在js中构造函数是用于创建特定类型对象的,在前面使用了Object原生构造函数创建对象,运行时可以直接在执行环境中使用。但其实,我们也可以进行自定义构造函数,用函数的形式为自己的对象定义属性和方法。

  1. // 构造函数模式
  2. function Person(name, age, city){
  3. this.name = name;
  4. this.age = age;
  5. this.city = city;
  6. this.sayName = function(){
  7. console.log(this.name);
  8. }
  9. }
  10. let p1 = new Person("yichuan",18,"Beijing")
  11. let p2 = new Person("onechuan",19,"Guangzhou")
  12. p1.sayName()//yichuan
  13. p2.sayName()//onechuan

构造函数模式相比于工厂模式而言,没有显式创建对象,属性和方法都是直接赋给this,也没有return返回任何值。

那么使用new构造函数的方式创建对象,具体会发生什么?

会有如下操作:

  • 1)在内存中开辟新的空间创建新对象
  • 2)这个新对象内部的proto特性被赋值为构造函数的prototype属性
  • 3)构造函数内部的this指向新对象
  • 4)给新对象添加属性
  • 5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

原型和原型链 - 图2

构造函数是什么?

其实构造函数也是函数,和普通函数没啥区别,只是调用的方式不同而已,通过new调用的函数是构造函数。构造函数定义的方法会在每个实例上都创建一遍,每次定义函数时,都会初始化一个对象。

  1. function Person(name, age, city){
  2. this.name = name;
  3. this.age = age;
  4. this.city = city;
  5. this.sayName = new Function("console.log(this.name);");
  6. }

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法,而这个对象就是通过调用构造函数创建的实例对象的原型,那么这个对象就叫做原型对象

原型对象的作用是:在原型对象上定义的属性和方法可以被对象实例所共享,即对象原型相当于一个存储公共属性和方法的容器。

等等,这不就是前面所说的构造器中直接赋值给对象实例的值吗?

其实不是,其实在进行构造函数Person定义时,构造函数内部是个空对象,没有任何属性和方法。然而,可以通过在Person的prototype上直接定义属性和方法,来挂载到Person对象的原型上。这样通过new Person()得到的对象实例是可以共享Person.prototype上的属性和方法。如下所示:

  1. function Person(){}
  2. Person.prototype.name = "yichuan";
  3. Person.prototype.age = 18;
  4. Person.prototype.city = "Beijing";
  5. Person.prototype.sayName = function(){
  6. console.log(this.name);
  7. }
  8. let p1 = new Person();
  9. p1.sayName();
  10. let p2 = new Person();
  11. p2.sayName();

原型和原型链

上面的原型模式中,已经将原型和原型对象的概念引出来了,那么我们重新整理下思路:

构造函数的prototype属性指向的原型对象中,定义了所有实例对象都能够共享的属性和方法,而不需要共享的属性和方法则直接定义在构造函数上。

通过构造函数创建的实例对象,会自动拥有原型对象上共享的属性或方法。

  1. function Person(name){
  2. this.name = name;
  3. }
  4. Person.prototype.address = "earth";
  5. const p = new Person("yichuan");//{name:age}
  6. const p1 = new Person("onechuan");

在上面代码中,在构造函数Person的prototype原型上定义了一个公共属性 Person.prototype.address="earth";,那么通过new出来的实例对象p和p1都会天生继承属性address,而p和p1各自的name值分别为"yichuan""onechuan"
原型和原型链 - 图3

函数与对象的关系

  • 函数是对象,对象都是通过函数创建的
  • 函数与对象并不是简单的包含与被包含的关系

原型的类别

  • 显式原型:prototype,是每个函数function独有的属性
  • 隐式原型:proto,是每个对象都具有的属性

原型和原型链 - 图4

原型和原型链

  • 原型:一个函数可以看做一个类,原型是所有类都有的一个属性,prototype原型的作用就是给这个类的每个对象都添加一个统一的方法
  • 原型链:每个对象都有一个proto,它指向它的prototype原型对象;它的prototype原型对象又有一个proto,指向它的prototype原型对象,就这样向上查找原型,直到顶级对象Object.prototype,最终指向是null

原型和原型链 - 图5

原型和原型链 - 图6

用图片描述原型链:

原型和原型链 - 图7

我们看到原型链的最终归属都是对象,而Object.prototype_proto_指向的是null,这是为了避免死循环而设置的,所以一切皆空。

参考文章

写在最后

  • 所有实例对象的proto都指向该构造函数的prototype原型对象 (即:p._proto_ === Person.prototype
  • 所有函数(包括构造函数)都是Function的实例,所有函数的_proto_都指向Function的原型对象
  • 所有的原型对象(包括 Function的原型对象)都是Object的实例,所以_proto_都指向 Object(构造函数)的原型对象。而Object构造函数的 _proto_指向 null
  • Function构造函数本身就是Function的实例,所以_proto_指向Function的原型对象。

最后的小结,摘自《【重点】图解:告诉面试官什么是 JS 原型和原型链?》一文。