MDN继承和原型链章笔记

1,181 阅读5分钟
原文 developer.mozilla.org/en-US/docs/…

JS并不提供类的实现(class关键字是在ES2015引入的但仅是语法糖,JS本身仍是基于原型的)。

当谈到继承,JS仅有一种结构:对象。每一个对象(object)都有一个指向其他对象的私有属性,这个属性叫做这个对象的原型(prototype)。

几乎所有的对象都是Object的实例,也可以说所有对象都继承自Object,它是原型链的顶端。Object.protorype指向null,null是没有prototype的。

当我们去访问某一个对象的属性时,JS会从此对象沿着原型链往上找,直到找不到为止。

Prototype:

----------以下重点---------最难理解的部分---------

obj.[[Prototype]]是ES标准的记法,obj.__proto__是浏览器厂商实际使用的记法,Object.getPrototypeOf(obj)/Object.setPrototypeOf(child, parent) 是ES6提供的访问器,这三种写法本质上是同一种东西。

someFunction.prototype跟上面的不是一种东西,它是一种抽象的泛指,表示所有用someFunction当作构造器创造出来的实例的__proto__。他俩的关系类似于class和instance的关系。

如果你仔细观察的话,会发现.prototype总是跟在function后面,.__proto__总是跟在创造出的对象后:

function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );
结果:
{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

当创建实例后

var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );

结果

{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

你可以看到,如果你找doSomeInstancing.__proto__,他指向的是doSomething.prototype。所以当你定义一个方法或类的时候,加在prototype上的属性会变成以后这个类的实例(或后代)的原型链__proto__上的属性。

实例化或者继承在JS中是一样的存在,都会是创造一个新的对象然后让他加入原型链。

当你尝试去在某对象中找属性,JS就会从这个对象开始遍历原型链直到最顶端,所以说继承的越多(原型链越长)性能越差。

当写任何一个东西的时候,JS会自动帮你添加原型链,这就是为什么说几乎所有的对象都是Object的实例。除非你用var freshObj = Object.create(null);创造出的就是没有继承Object的

var o = {a: 1};

// The newly created object o has Object.prototype as its [[Prototype]]
// o has no own property named 'hasOwnProperty'
// hasOwnProperty is an own property of Object.prototype. 
// So o inherits hasOwnProperty from Object.prototype
// Object.prototype has null as its prototype.
// o ---> Object.prototype ---> null
// 再次提醒:上面体现的是o.__proto__ === Object.prototype, 而不是o.prototype,这玩意是undefined

var b = ['yo', 'whadup', '?'];

// Arrays inherit from Array.prototype 
// (which has methods indexOf, forEach, etc.)
// The prototype chain looks like:
// b ---> Array.prototype ---> Object.prototype ---> null
// 同理你在用Array的实例,上面语法其实是new Array('yo', 'whadup', '?')

function f() {
  return 2;
}

// Functions inherit from Function.prototype 
// (which has methods call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null
// 这里在定义一个类或方法,所以上面是f.prototype === Function.prototype

需要用hasOwnProperty()检查某属性是否在这个对象本身定义的。

扩展原型链的4种方法:

1. new

function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto = new foo;
proto.bar_prop = "bar val";
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);

优:浏览器支持最广(除了ie5.5之前的),非常快,非常标准。

缺:new的函数必须是初始化过的。因为初始化时,他的构造器可能会存储每个对象必须生成的独特的信息,然而这个信息只会生成一次,所以会导致潜在的问题。

2. Object.create()

版本1:
function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto = Object.create(
  foo.prototype
);
proto.bar_prop = "bar val";
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);
版本2:使用create的第二个参数设置属性
function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto = Object.create(
  foo.prototype,
  {
    bar_prop: {
      value: "bar val"
    }
  }
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop)

优:大部分浏览器支持(除ie9之前)。允许直接设置__proto__,因为是一次性设置的所以浏览器能做相应的优化。能创造不继承Object的对象。

缺:如果添加了第二个参数的话,对象初始化过程性能会变很差。因为每个对象描述器(object-descriptor)属性有它自己的描述器(descriptor)对象。

3. Object.setPrototypeOf()

版本1:第一个参数就是child
function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto = {
  bar_prop: "bar val"
};
Object.setPrototypeOf(
  proto, foo.prototype
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);
版本2:返回值是child
function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto;
proto=Object.setPrototypeOf(
  { bar_prop: "bar val" },
  foo.prototype
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop)

优:大部分浏览器支持(除ie9之前)。能动态地操作对象原型,甚至强加prototype给无原型的对象(Object.create(null)创造的)。

缺:性能很差。浏览器大多会做对原型的优化且尝试去猜某个方法在内存的位置在你调用某实例之前,而这种动态添加原型的方法基本上毁了他们的优化甚至强迫某些浏览器重编译。

4. .__proto__

function foo(){}
foo.prototype = {
  foo_prop: "foo val"
};
function bar(){}
var proto = {
  bar_prop: "bar val",
  __proto__: foo.prototype
};
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);
版本2:从头到脚直接用proto
var inst = {
  __proto__: {
    bar_prop: "bar val",
    __proto__: {
      foo_prop: "foo val",
      __proto__: Object.prototype
    }
  }
};
console.log(inst.foo_prop);
console.log(inst.bar_prop)

优:大部分浏览器支持(除ie11之前)。把__proto__设置成非对象的值会隐式失败,不会抛异常。

缺:已被标准弃用(尽管还有浏览器支持),性能很差。跟上面一样,浏览器大多会做对原型的优化且尝试去猜某个方法在内存的位置在你调用某实例之前,而这种动态添加原型的方法基本上毁了他们的优化甚至强迫某些浏览器重编译。

看完MDN后的几点疑惑及思考

1. 为什么在文档上面提到不要func.prototype = {a: 'a'}这么写,因为会破坏原型链,而下面的好多例子又直接那么写呢?

// add properties in f function's prototype
f.prototype.b = 3;
f.prototype.c = 4;
// do not set the prototype f.prototype = {b:3,c:4}; this will break the prototype chain

因为如果你有很多层的继承的话,显而易见,你直接让他指向一个其他对象,那么前面的链就断了。可是例子里面都只有一层继承,他们上面都是Object.prototype,所以无所谓了


2. 为什么var b = {}; 或 b = []打印出来的b.prototype === undefined?

已经在前面讲过了,对象(实例)要用__proto__


3. 为啥说b = []的继承链上一层是Array.prototype呢, 从哪看出来的?

打印一下就知道了:



4. class(用class关键字声明的)怎么继承纯obj?

const Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

class Dog {
  constructor(name) {
    this.name = name;
  }
}

// If you do not do this you will get a TypeError when you invoke speak
Object.setPrototypeOf(Dog.prototype, Animal);

let d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.