JavaScript原型链继承的本质

263 阅读6分钟

前言

前几天参加了一场远程面试,面试的前期都很顺利,2面试官说的问题基本上都答的七七八八。

后面陷入了一种尴尬的境地,他们不断翻我的简历,然后皱眉提出了一个问题:我看你简历上有写,熟悉原生JavaScript及常见的设计模式,那你来简单实现一下new操作符

我当场不知所措,之前只背过new操作符的原理,手写就GG了,正经人谁手写这个啊?

于是面试后开始慢慢琢磨手写new操作符是个什么鬼,从头到尾又啃了一遍JavaScript的原型链继承

说起new操作符,就不得不提JavaScript的原型链和继承,我在2019年费了很大的劲去学这个东西,笔记也做了不少

image.png

最后也按照教程徒手画出来了完整的原型链结构图图:

这样的:😀

image.png

还有这样的:😃

image.png

不出意外,过了2年了,再回头看全忘了,这是什么东西,乱七八糟的

因为网上大多数教程都告诉我:原型它是这样的,它该这样,但几乎没有解释清楚它为什么是这样的?内部的原理是什么? 这也是我这么多年学了又忘,忘了又学,但还是根本学不会的根本原因。


原型链继承的定义

我们在网络上到处都读到 Javascript 具有原型继承的文章和教程。 然而,默认情况下,Javascript 仅通过 new 运算符提供原型继承。 因此,大多数解释读起来确实令人困惑。 本文旨在阐明什么是原型继承以及如何在 Javascript 上真正使用它。

当你在书上看到 Javascript 原型继承时,经常会看到这样的定义:

“ 当访问对象的属性时,如果对象没有该属性,JavaScript 会向上遍历原型链,直到找到该属性 ”

Javascript一般使用 proto 属性来表示原型链中的下一个对象。 下面将会详细说一下 proto 和原型之间的区别。

注意:proto 是不遵循ECMA标准的,不要在代码里使用。文中使用它来解释 Javascript 继承的工作原理。

下面伪代码展示了Javascript是如何根据原型链向上查找属性的:

function getProperty(obj, prop) {
  if (obj.hasOwnProperty(prop)) //如果自身有指定的属性直接返回
    return obj[prop]
  else if (obj.__proto__ !== null){ //如果没有,顺着原型链向上查找
    return getProperty(obj.__proto__, prop)
  }
  else{
    return undefined
  }
}

我们用例子演示一下原型链继承: 一个Point对象有两个坐标属性 x、y 和一个方法 print。

按照之前写的原型链的定义,我们创建一个新对象p,来继承Point对象的三个属性

var Point = {
  x: 10,
  y: 20,
  print: function () { 
    console.log(this.x, this.y);
  }
};
var p = {__proto__: Point} 
p.print(); // 10 20
console.log(p.x, p.y) // 10 20
__proto__: Point

注意上面的关键代码__proto__: Point,这个赋值的意思是:将p的__proto__指向Point

这样一来,p就会沿着原型链去Point寻找x,y和print属性,这就是原型链继承

容易误导人的原型链继承

令人困惑的是,很多文章都用__proto__这个定义教Javascript原型继承,但没有给出这个代码详细的解释。 相反,他们给出了这样的东西:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  print: function () { 
    console.log(this.x, this.y);
  }
};
 
var p = new Point(10, 20);
p.print(); // 10 20

这是什么东西?这与上面给出的代码完全不同。 Point 现在是一个函数,我们使用一个prototype 属性还有new 运算符。 这又是什么?怎么就完成了继承了?

这也是文章开头讲的面试盲点:new操作符工作的原理是什么

实现new操作符的过程

Javascript之父布莱登·艾克。在设计Javascript时,为了让它看起来足够像传统的面向对象的编程语言,比如C++和Java,使用new操作符来创建一个类的实例。所以它在Javascript中添加了new操作符

new 操作符采用函数 F 和参数:new F(arguments...)进行实例化,其中包括下面3个步骤:

  • 1. 创建类的实例。 它是一个空对象,其 __proto__ 属性设置为 F.prototype
  • 2. 初始化实例。 使用传递的参数调用函数F,并将F的this设置为实例
  • 3. 返回实例

既然了解了new操作符实例化的步骤,我们可以用代码来实现其过程。(这大概是迄今为止最简明易懂的解释了):

    //实现new操作符的过程
     function New (f) {
        /*1. 创建实例,将其__proto__指向F.prototype*/
       var n = { '__proto__': f.prototype }; 
       return function () {
        /*2. 使用传递的参数调用函数F,并将F的this设置为实例*/ 
         f.apply(n, arguments);
         /*3. 返回实例*/ 
         return n;
       };
     }

以上就是用代码实现new操作符的过程,来看看它能不能用

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  print: function () { console.log(this.x, this.y); }
};
 
var p1 = new Point(10, 20);
p1.print(); // 10 20
console.log(p1 instanceof Point); // true
 
var p2 = New (Point)(10, 20); //使用我们实现的New操作符函数
p2.print(); // 10 20
console.log(p2 instanceof Point); // true

可以看到,也能完美实现继承,new操作符内部也并没有那么复杂

实现真正意义的原型链继承

Javascript 规范只为我们提供了new操作符。 然而, 在Javascript还没有提供Object.create 函数之前,我们可以自定义Object.create 函数,来实现真正意义的原型链继承。

Object.create方法,是通过一个原始对象创建一个新的对象,新对象会继承原始对象的所有属性,这这个方法实现了最纯粹的原型继承,对象的属性来自于另一个对象

Object.create = function (oldObj) {
  function F() {}
  F.prototype = oldObj;
  var newObj = F()
  return newObj;
};

上面的代码实现了Object.create方法,虽说看起来很奇怪,但它的原理非常简单。 它只是创建一个新对象,将其原型设置为旧对象,然后返回新对象。

如果使用,__proto__,它可以这样写:

Object.create = function (oldObj) {
  var newObj = { '__proto__': oldObj };
  return newObj;
};

然后我们就可以实现对象的继承:

var Point = {
  x: 0,
  y: 0,
  print: function () { console.log(this.x, this.y); }
};
 
var p = Object.create(Point);
p.x = 10;
p.y = 20;
p.print(); // 10 20

仔细看下面的代码,可以发现newObj.__proto__ === F.prototype === oldObj

function F() {}
F.prototype = oldObj;
var newObj = F()
var newObj = { '__proto__': oldObj };

自定以的Object.create方法接受一个旧对象作为参数,并返回一个继承自旧对象的空新对象。 如果我们尝试从新对象中获取指定属性,但它缺少该属性,则新对象沿着原型链去旧对象中查找。 对象继承自对象,还有什么比这更面向对象的呢?

当然,也可以像下面这样定义Object.create方法,使用this关键字来代替旧对象,其原理和使用__proto__一样

Object.prototype.begetObject = function () {
    function F() {}
    F.prototype = this;
    return new F();
};

newObject = oldObject.begetObject();