js的原型、原型链和继承(面试高频考点)

317 阅读11分钟

继承与原型链

//1.基于类的语言有Java/C++,但JS是基于原型的,JS是动态的,本身不提供class的实现。虽在 ES2015/ES6中引入class关键字,但只是语法糖,JS仍是基于原型的。
//2.继承------JS只有一种结构:对象。(重点重点重点)
    //每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(**prototype**)。
    //该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为null。
    //根据定义,null没有原型,并作为这个**原型链**中的最后一个环节。
//3.几乎所有JS中的对象都是位于原型链顶端的Object的实例。
//4.原型继承通常被认为是JS的弱点之一,但原型继承模型本身实际比经典模型更强大。如在原型模型的基础上构建经典模型相当简单。

1. 基于原型链的继承

1. 继承属性
    //JS对象是动态的属性“包”(指其自己的属性)。
    //JS对象有一个指向一个原型对象的链。
    //当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
    //遵循ECMAScript标准,someObject.[[Prototype]]符号是用于指向someObject的原型。
    //从ECMAScript6开始,[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。
    //这个等同于JS的非标准但许多浏览器实现的属性__proto__。
    //但它不应该与构造函数func的prototype属性相混淆。
    //被构造函数创建的实例对象的[[Prototype]]指向func的prototype属性。Object.prototype属性表示Object的原型对象。
    
image.png
    //这里演示当尝试访问属性时会发生什么:
    //让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:
    let f = function () {
       this.a = 1;
       this.b = 2;
    }
    /* 这么写也一样
    function f() {
      this.a = 1;
      this.b = 2;
    }
    */
    let o = new f(); // {a: 1, b: 2}
    
    //在 f 函数的原型上定义属性
    f.prototype.b = 3;
    f.prototype.c = 4;

    // 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
    // o.[[Prototype]] 有属性 b 和 c
    //  (其实就是 o.__proto__ 或者 o.constructor.prototype)
    // o.[[Prototype]].[[Prototype]] 是 Object.prototype.
    // 最后 o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null
    // 这就是原型链的末尾,即 null,
    // 根据定义,null 就是没有 [[Prototype]]。
    // 综上,整个原型链如下:
    // {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

    console.log(o.a); // 1
    // a 是 o 的自身属性吗?是的,该属性的值为 1

    console.log(o.b); // 2
    // b 是 o 的自身属性吗?是的,该属性的值为 2
    // 原型上也有一个'b'属性,但是它不会被访问到。
    // 这种情况被称为"属性遮蔽 (property shadowing)"

    console.log(o.c); // 4
    // c 是 o 的自身属性吗?不是,那看看它的原型上有没有
    // c 是 o.[[Prototype]] 的属性吗?是的,该属性的值为 4

    console.log(o.d); // undefined
    // d 是 o 的自身属性吗?不是,那看看它的原型上有没有
    // d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
    // o.[[Prototype]].[[Prototype]] 为 null,停止搜索
    // 找不到 d 属性,返回 undefined


    //代码来源链接:<https://repl.it/@khaled_hossain_code/prototype>
    //给对象设置属性会创建自有属性。获取和设置行为规则的唯一例外是当继承的属性带有getter或setter时。

2. 继承方法

//JS没有其他基于类的语言所定义的“方法”。
//在JS里,任何函数都可以添加到对象上作为对象的属性。
//函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
//当继承的函数被调用时,this指向的是当前继承的对象,而不是继承的函数所在的原型对象。
    var o = {
      a: 2,
      m: function(){
        return this.a + 1;
      }
    };

    console.log(o.m()); // 3
    // 当调用 o.m 时,'this' 指向了 o.

    var p = Object.create(o);
    // p是一个继承自o的对象
    // Object.create()方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

    p.a = 4; // 创建 p 的自身属性 'a'
    console.log(p.m()); // 5
    // 调用 p.m 时,'this' 指向了 p
    // 又因为 p 继承了 o 的 m 函数
    // 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'

3. 在JS中使用原型

//在JS,函数(function)是允许拥有属性的。
//所有的函数会有一个特别的属性 —— `prototype` 。


//强烈建议打开浏览器的控制台(在 Chrome 和火狐浏览器中,按 Ctrl+Shift+IF12),进入“console”选项卡,把如下的JS代码复制粘贴到窗口中,最后通过按下回车键运行代码。
function doSomething(){}
console.log( doSomething.prototype );
// 和声明函数的方式无关,
// JS中的函数永远有一个默认原型属性。
var doSomething = function(){};
console.log( doSomething.prototype );
//结果如下:
{
    constructor: ƒ(),
    [[Prototype]]: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}


//我们可以给 doSomething 函数的原型对象添加新属性,如下:
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );
//结果如下:
{
    foo: "bar",
    constructor: ƒ(),
    [[Prototype]]: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}


//通过 new 操作符来创建基于这个原型对象的 doSomething 实例。
//使用 new 操作符,只需在调用 doSomething 函数语句之前添加 new。
//便可以获得这个函数的一个实例对象。一些属性就可以添加到该原型对象中。
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );
//运行的结果:
    doSomething {
    prop: "some value",
    [[Prototype]]: {
        foo: "bar",
        constructor: ƒ doSomething(),
        [[Prototype]]: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
     }
   }
//doSomeInstancing中的[[Prototype]]是doSomething.prototype. 
//但这是做什么的呢?当你访问doSomeInstancing中的一个属性,浏览器首先会查看doSomeInstancing中是否存在这个属性。



//doSomeInstancing不包含属性信息,那么浏览器会在doSomeInstancing的[[Prototype]]中进行查找 (同 doSomething.prototype). 如属性在doSomeInstancing的[[Prototype]]中查找到,则使用doSomeInstancing中[[Prototype]]的属性。
//否则,如果doSomeInstancing中[[Prototype]]不具有该属性,则检查doSomeInstancing的[[Prototype]][[Prototype]]是否具有该属性。
//默认情况下,任何函数的原型属性[[Prototype]]都是window.Object.prototype.
//通过doSomeInstancing的[[Prototype]][[Prototype]]( 同 doSomething.prototype 的[[Prototype]](同Object.prototype)) 来查找要搜索的属性。
//如果属性不存在doSomeInstancing的[[Prototype]][[Prototype]]中,那么就会在doSomeInstancing的[[Prototype]][[Prototype]][[Prototype]]中查找。
//然而,这里存在个问题:doSomeInstancing的[[Prototype]][[Prototype]][[Prototype]]其实不存在。
//因此,只有这样,在[[Prototype]]的整个原型链被查看之后,这里没有更多的[[Prototype]],浏览器断言该属性不存在,并给出属性值为undefined的结论。
function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:      " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo:       " + doSomeInstancing.foo);
console.log("doSomething.prop:           " + doSomething.prop);
console.log("doSomething.foo:            " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo:  " + doSomething.prototype.foo);
//结果如下:
doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

4. 使用不同的方法来创建对象和生成原型链

1. 使用语法结构创建的对象
var o = {a: 1};
// o 这个对象继承了 Object.prototype 上面的所有属性
// o 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 o 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// o ---> Object.prototype ---> null



var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null



function f(){
  return 2;
}
// 函数都继承于 Function.prototype
// (Function.prototype 中包含 call, bind 等方法)
// 原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null



2.  使用构造器创建的对象
//在JS中,构造器其实就是一个普通的函数。
//当使用new 操作符来作用这个函数时,它就可以被称为构造方法(构造函数)。
function Graph() {
  this.vertices = [];
  this.edges = [];
}
Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};
var g = new Graph();
console.log(g)
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
//结果如下:
Graph {
    edges: []
    vertices: []
    [[Prototype]]: {
        addVertex: ƒ (v)
        [[Prototype]]:{
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}


3.  使用 Object.create 创建的对象
//ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。
//新对象的原型就是调用 create 方法时传入的第一个参数:
var a = {a: 1}; // a ---> Object.prototype ---> null

var b = Object.create(a); // b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b); // c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null); // d ---> null
console.log(d.hasOwnProperty); // undefined,因为 d 没有继承 Object.prototype


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

//"use strict"使用严格模式;
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;
  }
}
var square = new Square(2);
console.log(square)
结果如下:
image.png
5.  性能
//在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。
//另外,试图访问不存在的属性时会遍历整个原型链。
//遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。
//要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从Object.prototype继承的hasOwnProperty方法。
function Graph() {
  this.vertices = [];
  this.edges = [];
}
Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};
var g = new Graph();
console.log(g.hasOwnProperty('vertices')); // true
console.log(g.hasOwnProperty('nope'));  // false
console.log(g.hasOwnProperty('addVertex')); // false
console.log(g.Prototype.hasOwnProperty('addVertex'));  // true
//hasOwnProperty是JS中唯一一个处理属性并且不会遍历原型链的方法。
//另一种这样的方法:Object.keys()会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。
const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};
console.log(Object.keys(object1)); /Array ["a", "b", "c"]
//注意:检查属性是否为undefined是不能够检查其是否存在的。
//该属性可能已存在,但其值恰好被设置成了 undefined。



6.  错误实践:扩展原生对象的原型
//经常使用的一个错误实践是扩展Object.prototype或其他内置原型。
//这种技术被称为猴子补丁并且会破坏封装。
//尽管一些流行的框架(如 Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。
//扩展内置原型的唯一理由是支持JS引擎的新特性,如Array.forEach。

5. prototype 和 Object.getPrototypeOf

//JS完全是动态的,都是运行时,而且不存在类(class)。
//所有的都是实例(对象)。即使我们模拟出的“类”,也只是一个函数对象。


//function A有一个叫做prototype的特殊属性。该特殊属性可与JS的new操作符一起使用。
//对原型对象的引用被复制到新实例的内部[[Prototype]]属性。
//如当执行var a1 = new A();时,JS(在内存中创建对象之后,和在运行函数A()把this指向对象之前)设置a1.[[Prototype]] = A.prototype;。
//然后当您访问实例的属性时,JS首先会检查它们是否直接存在于该对象上,如果不存在,则会[[Prototype]]中查找。
//这意味着你在prototype中定义的所有内容都可以由所有实例有效地共享,你甚至可以稍后更改部分prototype,并在所有现有实例中显示更改(如果有必要的话)。


//像上面的例子中,如果你执行var a1 = new A(); var a2 = new A();
//那么a1.doSomething事实上会指向Object.getPrototypeOf(a1).doSomething,它就是你在A.prototype.doSomething中定义的内容。
//也就是说:Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething
//(补充:实际上,执行a1.doSomething()相当于执行Object.getPrototypeOf(a1).doSomething.call(a1)==A.prototype.doSomething.call(a1))
//简而言之,prototype是用于类的,而Object.getPrototypeOf()是用于实例的(instances),两者功能一致。
//[[Prototype]]看起来就像递归引用,
//如a1.doSomething、Object.getPrototypeOf(a1).doSomething、Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething等等,直到它被找到或Object.getPrototypeOf返回null。

//因此,当你执行:
var o = new Foo();


//JavaScript 实际上执行的是:
var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);


//(或者类似上面这样的),然后,当你执行:
o.someProp;
//它检查 o 是否具有someProp属性。
//如果没有,它会查找Object.getPrototypeOf(o).someProp,
//如果仍旧没有,它会继续查找Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp

6. 结论

//在使用原型继承编写复杂代码之前,理解原型继承模型是**至关重要**的。
//注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。
//此外,原生原型不应该被扩展,除非它是为了与新的JS特性兼容。

7. 示例

image.png
//B继承自A:
function A(a){
  this.varA = a;
}
// 以上函数 A 的定义中,既然 A.prototype.varA 总是会被 this.varA 遮蔽,
// 那么将 varA 加入到原型(prototype)中的目的是什么?
A.prototype = {
  varA : null,
   //既然它没有任何作用,干嘛不将 varA 从原型(prototype)去掉 ?
   //也许作为一种在隐藏类中优化分配空间的考虑 ?
   //https://developers.google.com/speed/articles/optimizing-javascript
   //如果 varA 并不是在每个实例中都被初始化,那这样做将是有效果的。
  doSomething : function(){
    // ...
  }
}
function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null,
    enumerable: true,
    configurable: true,
    writable: true
  },
  doSomething : {
    value: function(){ // override
      A.prototype.doSomething.apply(this, arguments);
      // call super
      // ...
    },
    enumerable: true,
    configurable: true,
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();
//最重要的部分是:
//   类型被定义在.prototype中
//   用Object.create()来继承