原生JS笔记(对象、原型、继承)

134 阅读3分钟

JavaScript 是通过「原型」来创建对象。下面记录了关于 JavaScript 对象、原型、继承相关知识。

原型

每一个对象都具有一个 prototype 属性,它用于实现「原型继承」和「属性共享」,它所指向的那个对象也就是这个对象的原型。

[[prototype]]隐式原型

任何一个对象都具有一个 [[prototype]] 内部属性,它可能是 null 或者是对象,不可以被外部直接访问,但是有些方法可以访问到。比如 getPrototypeOf(mdn)、setPrototypeOf(mdn)。

另外,改变一个对象的 [[Prototype]] 属性,这种行为在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的(来源 proto mdn

proto

__proto__ 属性, 通过它访问的对象的内部[[Prototype]]。注意它只是一个被浏览器默认支持的行为,在 ECMAScript 2015 规范中被标准化为传统功能(位置),以确保 Web 浏览器的兼容性。

__proto__ 属性值指向它的构造函数的 prototype 属性值

Function.prototype.prototype

当一个函数被 new 运算符调用时,构造函数的 prototype属性会变成实例对象的 prototype。

注意:

  1. Generator 比较特殊,不可以被new 调用。
  1. 有些函数可能有原型,但在用 new 调用时会无条件抛出。 例如,Symbol() 和 BigInt() ,因为 Symbol.prototype 和 BigInt.prototype 仅旨在为原始值提供方法,但不应直接构造包装对象。

Object.prototype.constructor

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

function P() {}
P.prototype.constructor === P // true

Class 的 prototype 属性和__proto__属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

原型链

每一个对象都有他们的原型,它们的原型也是对象,因此就会形成一个回溯原型的链条,最终追溯到Object的原型null。这就是原型链。

Object.getPrototypeOf(Object.prototype)  // null

继承

如何实现多重继承

JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 继承 M1
S.prototype = Object.create(M1.prototype); // todo 为什么用 object.create
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定构造函数
S.prototype.constructor = S; // todo 为什么要修改 constructor 

var s = new S();
s.hello // 'hello'
s.world // 'world'

上面代码中有几个问题我们要讨论一下:

  1. 为什么用了 Object.create

为了避免重复调用「父类构造函数」,比如下面的代码

function M1() {
  console.log("M1 constructor");
  this.hello = "hello";
}

function S() {
  M1.call(this);
}
S.prototype = new M1();
// S.prototype = Object.create(M1.prototype)
S.prototype.constructor = S;
var s = new S();
console.log(s.hello);
 //  结果
 // M1 constructor
 // M1 constructor
 // hello
  1. 为什么要修改 constructor

因为之前修改 prototype 的时候,已经覆盖了 constructor,如果不修改会导致 instanceof 出问题。

function M1() {
  console.log("M1 constructor");
  this.hello = "hello";
}

function S() {
  M1.call(this);
}
S.prototype = new M1();
var s = new S();
const isInstance = s instanceof M1 
console.log(isInstance); // true 
  1. 为什么要执行父类构造函数

生成对象四步,其中一步是执行构造函数。

参考链接:

1 MDN 原型链和继承

2 ES5 对象简介

3 ecma Object.prototype.proto

4 网道 类的 prototype 属性和__proto__属性

5 Object.prototype.constructor

6 Function.prototype.prototype