继承与原型链:原型链的尽头是null?

8,741 阅读5分钟

面不面试都要会的继承与原型链:原型链的尽头是null?

tips:每个技术点都值得优学优写:9期

好文推荐:

约2万字-Vue源码解读汇总篇(续更)

前端要会打组合拳,复盘30+技术点打出的功能

前言

我们知道,es 有一个关键字叫 class ,这是在 es2015(es6) 的时候引入的。它的中文意思是“类”, 在像 java 这样的编程语言中, 原生就有类(class)的支持,但 es6 引入的 class 关键字, 也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。 每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

继承属性与原型链

javaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

在原型链上,属性的被传递拥有,就是一种继承现象。(使用继承关键字 extends 的继承暂且不谈)

下面通过一个示例来理解继承与原型链

    // 使用语法结构创建的对象
    let obj = {
      a: 'hello',
      fc: function () {
        return this.a + 1
      }
    }
    // obj 这个对象继承了 Object.prototype 上面的所有属性
    // obj 自身没有名为 hasOwnProperty 的属性
    // hasOwnProperty 是 Object.prototype 的属性
    // 因此 obj 继承了 Object.prototype 的 hasOwnProperty
    // Object.prototype 的原型为 null

    // 原型链如下:
    // obj ---> Object.prototype ---> null
    console.log(obj)
    console.log(obj.__proto__)
    console.log(obj.__proto__.__proto__) // null,这就是原型链的终点

    let subObj = Object.create(obj); // 创建的 subObj 本身没有自身的属性,但它继承有 obj 的属性。
    console.log(subObj.fc()); // 3,fc() 继承自 obj
    // 那么此时,我们就说 subObj 是一个继承自 obj 的对象,因为继承,obj 的一些属性被传递给了 subObj,
    // 例如 fc() 就继承自 obj
    // subObj 的原型链是 // subObj ---> obj ---> Object.prototype ---> null
    console.log(subObj)

这是一张打印上面 obj 对象的原型链截图, 打印结果也符合上面关于 obj 对象原型链的结论:obj ---> Object.prototype ---> null

image.png

下面是一张打印 subObj 的原型连截图: 打印结果也符合上面关于 subObj 对象原型链的结论:subObj ---> obj ---> Object.prototype ---> null

image.png

创建对象和生成原型链的几种方法

使用 Object.create 创建的对象

Object.create() 是 es5 引入的创建对象的方法,使用该方法创建的新对象的原型就是 Object.create() 传入的第一个参数。

下面是一个例子。

let a = {a: 1};
// 原型链: a ---> Object.prototype ---> null

let b = Object.create(a);
// 原型链:b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 ,b并没有自身属性,b.a 属性是继承而来的

let c = Object.create(b);
// 原型链:c ---> b ---> a ---> Object.prototype ---> null

let d = Object.create(null);
// 原型链:d ---> null // 根据定义,null 没有原型,因此 null 就是原型链的最后一个环节
console.log(d.hasOwnProperty); 
// undefined, 因为 d 没有继承 Object.prototype,所以没有 hasOwnProperty 属性。

使用语法结构创建的对象

例如像下面这样,没用通过关键字创建的

let obj = {a: 1};

// obj 这个对象继承了 Object.prototype 上面的所有属性
// obj 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 obj 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// obj ---> Object.prototype ---> null

使用构造器创建的对象

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

下面是一个示例

function Obj() {
  this.arr = [];
  this.arr2 = [];
}

Obj.prototype = {
  addArr: function(v){
    this.arr.push(v);
  }
};

let g = new Obj();
// g 是生成的对象,他的自身属性有 'arr' 和 'arr2'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Obj.prototype。

// 原型链如下:
// g ---> obj ---> Object.prototype ---> null

使用 class 关键字创建的对象

ES6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。 JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。

看见了吗? js 不仅有 class(类) 关键字,还有 extends(继承) 关键字。

下面是一个来自 Mozilla 的示例

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

let square = new Square(2);

关于性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性, 则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty方法。

小结

    1. JavaScript 仍然是基于原型的,尽管有 class 和 extends 等关键字。
    1. 没错,原型链的最后一个环节是 null,null 是原型链的终点。
    1. 理解原型继承模型对于深入理解 JavaScript 是至关重要的,它是 JavaScript 的高级部分内容。
    1. 有一种继承是基于原型链的继承,这种继承当然也具有获得被继承者的属性的能力。