JavaScript - 重新认识构造函数、原型、原型链、继承

320 阅读20分钟

前言

构造函数

什么是构造函数

构造函数 就是提供一个生成对象的模板并描述对象基本结构的函数。一个构造函数可以生成多个对象,这些对象都有相同的结构

构造函数 本身就是一个函数,不过为了规范一般将其首字母大写。构造函数普通函数 的区别在于使用 new 生成实例的函数就是构造函数,直接调用的就是普通函数

生成对象实例时必须使用 new 命令来调用构造函数,所以构造函数更合理的理解应该是 函数的构造调用

constructor 返回创建实例对象时构造函数的引用,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串

function Person(age) {
    this.age = age;
}

var p = new Person(18);
p.constructor === Person; // true
p.constructor === Object; // false

那普通函数创建的实例是不是一定没有 constructor 属性呢?不一定

// 普通函数
function person(age) {
    this.age = age;
}
var p = person(20); // undefined
p.constructor; // Cannot read property 'constructor' of undefined

// 普通函数
function person(age) {
    return {
        age: age
    }
}
var p = person(20);
p.constructor === Object; // true

Symbol 是否是构造函数

MDN 是这样介绍的

The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()"

Symbol 是基本数据类型,但作为构造函数来说它并不完整,因为它不支持语法 new Symbol(),因此认为其不是构造函数,若要生成实例直接使用 Symbol() 即可(来自 MDN),每个从 Symbol() 返回的值都是唯一的

new Symbol(1); // Symbol is not a constructor 
Symbol(1); // Symbol(1)

虽然是基本数据类型,但 Symbol(1) 实例可以获取 constructor 属性值

var a = Symbol(1);  // Symbol(1)
console.log(a.constructor); // ƒ Symbol() { [native code] }

这里的 constructor 属性其实是 Symbol 原型上的,即 Symbol.prototype.constructor 返回创建实例原型的函数, 默认为 Symbol 函数

constructor 值是否只读

对于引用类型来说 constructor 属性值是可以修改的,但对于基本类型来说是只读的

引用类型情况其值可修改这个很好理解,比如原型链继承方案中就需要对 constructor 重新赋值进行修正

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 设置 Bar 的 prototype 属性为 Foo 的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello JS';

Bar.prototype.constructor === Object; // true
var test = new Bar() // 创建 Bar 的一个新实例
console.log(test); 

image.png

// 修正 Bar.prototype.constructor 为 Bar 本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建 Bar 的一个新实例
console.log(test);

image.png

对于基本类型来说是只读的,如 1"1"trueSymbolnullundefined 是没有 constructor 属性的)

function Type() {};
const types = [1, "1", true, Symbol(1)];

for(let i = 0, len = types.length; i < len; i ++) {
  types[i].constructor = Type;
  types[i] = [types[i].constructor, types[i] instanceof Type, types[i].toString()];
};
console.log(types.join("\n"));

为什么呢?因为创建它们的是只读的原生构造函数 native constructors,这个例子也说明了依赖一个对象的 constructor 属性并不安全

模拟实现 new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 ——(来自于MDN)

当代码 new Foo(...) 执行时,会发生以下事情:

  • 一个继承自 Foo.prototype 的新对象被创建
  • 使用指定的参数调用构造函数 Foo 并将 this 绑定到新创建的对象上(new Foo 等同于 new Foo(),即没有指定参数列表,Foo 不带任何参数调用的情况)
  • 由构造函数返回的对象就是 new 表达式的结果,若构造函数没有显式返回一个对象,则使用步骤 1 创建的对象
// 第一版
function createNew() {
  // 创建一个空的对象
  let obj = new Object(); 
  // 获得构造函数,arguments 中去除第一个参数
  const Con = [].shift.call(arguments);
  // 链接到原型
  // 创建一个原型为构造器的 prototype 的空对象 obj
  // const obj = Object.create(constructor.prototype);  
  obj.__proto__ = Con.prototype;
  // 绑定 this 实现继承,使用 apply 改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  Con.apply(obj, arguments);
  // 返回对象
  return obj;
};

测试一下

function Car(color) {
    this.color = color;
}
Car.prototype.start = function() {
    console.log(this.color + " car start");
}

var car = createNew(Car, "black");
car.color;
// black

car.start();
// black car start

上面的代码已经实现了 80%,现在继续优化。构造函数返回值有如下三种情况:

  • 返回一个对象 this 失效,实例 person 中只能访问到返回对象中的属性
function Person(age, name) {
    this.age = age;
    return {
        name: name
    }
}

const person = new Person(18, "tn");
person.age; // undefined
person.name; // "tn"
  • 没有 return,即返回 undefined
    实例 person 中只能访问到构造函数中的属性,和上面完全相反
function Person(age, name) {
    this.age = age;
}

const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
  • 返回 undefined 以外的基本类型
    实例 person 中只能访问到构造函数中的属性,和情况 1 相反,结果相当于没有返回值
function Person(age, name) {
    this.age = age;
    return "new person";
}

const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined

所以需要判断下返回值是不是一个对象,若是对象则返回该对象,不然返回新创建的 obj 对象,实现代码如下

// 第二版
function createNew() {
  // 创建一个空的对象
  let obj = new Object();
  // 获得构造函数,arguments 中去除第一个参数
  const Con = [].shift.call(arguments);
  // 链接到原型,obj 可以访问到构造函数原型中的属性
  obj.__proto__ = Con.prototype;
  // 绑定 this 实现继承,obj 可以访问到构造函数中的属性
  const ret = Con.apply(obj, arguments);
  // 优先返回构造函数返回的对象
  return ret instanceof Object ? ret : obj;
};

函数对象和普通对象

经常看到一句话说:万物皆对象,对象就是属性的集合(对象里面的一切都是属性,只有属性没有方法,方法也是一种属性,因为它的属性表示为键值对的形式)。而在 JavaScript 中,创建对象有几种方式,如对象字面量 、通过构造函数 new 一个对象、Object.create() image.png

image.png

暂且先不管上面的代码有什么意义,至少能看出都是对象且却存在着差异性

其实在 JavaScript 中可以将对象分为函数对象普通对象

  • 函数对象就是 JavaScript 中用函数来模拟的类实现,如 ObjectFunction 就是典型的函数对象 下述代码中 obj1、obj2、obj3、obj4 都是普通对象,fun1、fun2、fun3 都是 Function 的实例,即函数对象
function fun1() {};
const fun2 = function() {};
const fun3 = new Function('name','console.log(name)');

const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();

console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
console.log(typeof obj4); // object

因此,所有 Function 的实例都是函数对象,其他均为普通对象,其中包括 Function 实例的实例

function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)

JavaScript 中万物皆对象,而对象皆出自构造(构造函数) 我的理解是所有对象都是由 new 操作符后跟函数调用来创建的,字面量表示法只是语法糖(即本质也是 new,功能不变、使用更简洁),无论是 function Foo() 还是 let a = { b : 1 }

对于创建一个对象来说,更推荐使用字面量的方式创建,因为使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是使用字面量的方式就没这个问题

function Foo() {};
// function 就是个语法糖
// 内部等同于 new Function()

let a = { b: 1 };
// 这个字面量内部也是使用了 new Object()

NumberStringBooleanArrayObjectFunctionDateRegExpError 等都是函数,而且是内置的原生构造函数,在运行时会自动出现在执行环境中

构造函数是为了创建特定类型的对象,这些通过同一构造函数创建的对象有相同原型,共享某些方法。如:所有的数组都可以调用 push 方法,因为它们有相同原型

原型和原型链都是来源于对象而服务于对象的概念

原型(prototype)

JavaScript 常被描述为一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样

每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身

prototype - object that provides shared properties for other objects

在规范里,prototype 被定义为:给其它对象实例提供共享属性的对象。因此 prototype 自己本身也是对象,只是被用以承担某个职能罢了

只有函数才拥有该属性,它是 function 对象的一个显式原型属性,当声明一个函数时该属性就被自动创建了,它定义了构造函数制造出来的对象实例的公共祖先,通过该构造函数产生的对象可以继承该原型上的属性和方法

基本上所有函数都有这个属性,但是也有一个例外,若用以下方法创建一个函数,可发现这个函数是不具有 prototype 属性。因为 Function.prototype 是引擎创建出来的函数对象,引擎认为不需要给这个对象添加 prototype 属性

let fun = Function.prototype.bind();

prototype 上面添加属性和方法,每个构造出来的对象实例都可继承这些属性和方法。虽然每个对象都是独立的,但它们都有共同的祖先,当访问这个对象的属性时,若对象本身没有该属性,则会往上找到它的原型,然后在原型上访问这个属性

constructor

prototype 有个默认属性 constructor,指向一个函数,这个函数就是该对象的构造函数

Person.prototype.constructor === Person // true

constructor 是个公有且不可枚举属性,一旦改变了函数的 prototype,那新对象就没有这个属性(可通过原型链取到 constructor

image.png

注意,每个对象都有其对应的构造函数,本身或者继承而来。单从constructor 这个属性来讲只有 prototype 对象才有。每个函数在创建时 JavaScript 会同时创建一个该函数对应的 prototype 对象

函数创建的对象.__proto__ === 该函数.prototype
函数.prototype.constructor === 该函数本身

故通过函数创建的对象即使自己没有 constructor 属性,它也能通过 __proto__ 找到对应的constructor,所以任何对象最终都可以找到其对应的构造函数

其实这个属性可以说是一个历史遗留问题,它有两个作用

  • 让实例对象知道是什么函数构造了它
  • 若想给某些类库中的构造函数增加一些自定义的方法,就可以通过 xx.constructor.method 来扩展

__proto__

1、首先需要明确:__proto__constructor 是对象独有的;prototype 是函数独有的
2、但在 JavaScript 中,函数也是对象,因此函数也拥有 __proto__constructor 属性

每个对象都有该隐式原型属性,指向了原型(若是构造函数创建的对象,则指向创建该对象的构造函数的原型)

这里用 __proto__ 获取对象的原型,__proto__ 是每个对象实例上都有的属性,prototype是构造函数的属性,这两个并不一样,但 __proto__prototype 指向同一个对象

image.png

__proto__ 指向了 [[prototype]](一个对象或 null),因 [[prototype]] 是内部属性,并不能从外部访问到,因此有些浏览器实现了 __proto__ 来访问

因此,ECMAScript 规范说 prototype 应当是一个隐式引用:

  • 通过 ES6 新增的 Object.getPrototypeOf(obj) 访问指定对象的 prototype 对象
  • 通过 Object.setPrototypeOf(obj, anotherObj) 设置指定对象的 prototype 对象
  • 部分浏览器实现了 __proto__ ,使得可以通过 obj.__proto__ 直接访问原型,通过 obj.__proto__ = anotherObj 直接设置原型
  • ES6 规范只好向事实低头,将 __proto__ 属性纳入了规范的一部分,以确保 Web 浏览器的兼容性

__proto__ 属性既不能被 for...in 遍历出来,也不能被 Object.keys(obj) 查找出来

其实 __proto__ 是个定义在 Object.prototype 上的访问器属性,即用 gettersetter 定义的属性,访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法

Object.defineProperty(Object.prototype,'__proto__',{
  get() {
    console.log('get')
  }
});
({}).__proto__;
console.log((new Object()).__proto__);
// get
// get

const weakMap = new WeakMap();
Object.prototype = {
  get __proto__() {
    return this['[[prototype]]'] === null ? weakMap.get(this) : this['[[prototype]]'];
  },
  set __proto__(newPrototype) {
    if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);

    const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
    if (newPrototype === null || isObject) {
      // 如果之前通过 __proto__ 设置成 null
      // 此时再通过给 __proto__ 赋值的方式修改原型都是徒劳
      // 表现就是 obj.__proto__ = { a: 1 } 就像一个普通属性 obj.xxx = { a: 1 }
      if (this['[[prototype]]'] === null) {
        weakMap.set(this, newPrototype);
      } else {
        this['[[prototype]]'] = newPrototype;
      }
    }
  },
  // ... 其它属性如 toString,hasOwnProperty 等
};

__proto__ 属性在 ES6 时被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题,为了更好的支持,推荐使用 Object.getPrototypeOf()

若一个对象的 __proto__ 属性被赋值为 null,这时它的原型确实已经被修改为 null,但想再通过对 __proto__ 赋值的方式设置原型时是无效的,这时 __proto__ 和一个普通属性没有区别,只能通过 Reflect.setPrototypeOfObject.setPrototypeOf 才能修改原型。Reflect.setPrototypeOf 之所以能修改原型是因为它是直接修改对象的原型属性,即内部直接对对象的 [[prototype]] 属性赋值,而不会通过 __proto__getter

const obj = { name: 'xiaoming' };

obj.__proto__ = null;
console.log(obj.__proto__); // => undefined
console.log(Reflect.getPrototypeOf(obj)); // => null

// 再次赋值为 null
obj.__proto__ = null;
console.log(obj.__proto__); // => null

obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
// __proto__ 就像一个普通属性一样 obj.xxx = { a: 1 }
// 并没有将原型设置成功
console.log(Reflect.getPrototypeOf(obj)); // => null

Reflect.setPrototypeOf(obj, { b: 2 });
// __proto__ 被设置为 null 后,obj 的 __proto__ 属性和一个普通的属性没有区别
console.log(obj.__proto__); // => { a: 1 }
// 使用 Reflect.setPrototypeOf 是可以设置原型的
console.log(Reflect.getPrototypeOf(obj)); // => { b: 2 }

通过改变一个对象的 [[Prototype]] 属性来改变和继承属性会对性能造成非常严重的影响且性能消耗的时间也不是简单的花费在 obj.__proto__ = ... 语句上,它还会影响到所有继承自该 [[Prototype]] 的对象,若关心性能就不应该修改一个对象的 [[Prototype]]

若要读取或修改对象的 [[Prototype]] 属性,建议使用如下方案,但是此时设置对象的 [[Prototype]] 依旧是一个缓慢的操作,若性能是一个考虑问题,就要避免这种操作

// 获取
Object.getPrototypeOf();
Reflect.getPrototypeOf();

// 修改
Object.setPrototypeOf();
Reflect.setPrototypeOf();

__proto__ 存在于所有的对象上,是对象所独有的且指向它的原型对象。它的作用就是当你在访问一个对象属性时,若该对象内部不存在这个属性,则会去它的 __proto__ 属性所指向的对象(原型对象,原型也是对象也有它自己的原型)上查找,若原型对象依旧不存在这个属性,则去其原型对象的 __proto__ 属性所指向的原型对象上去查找...以此类推,直到找到 null,返回 undefined,这个查找的过程也就构成了我们常说的 原型链

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 __proto__ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性

image.png

Object.create()

之前说对象的创建方式主要有两种,一种是 new 操作符后跟函数调用,另一种是字面量表示法

第三种就是 ES5 提供的 Object.create() 方法,该方法会创建一个新对象,第一个参数接收一个对象,将会作为与新创建对象关联的原型对象,第二个可选参数是属性描述符(不常用,默认是 undefined

平常所看到的空对象其实并不是严格意义上的空对象,它的原型对象指向Object.prototype,还可以继承 hasOwnPropertytoStringvalueOf 等方法

若要创建一个新对象同时继承另一个对象的 [[Prototype]] ,推荐使用 Object.create()

若想生成一个不继承任何属性的对象,可使用 Object.create(null)

若想生成一个平常字面量方法生成的对象,需要将其原型对象指向Object.prototype

let obj = Object.create(Object.prototype);
// 等价于
let obj = {};
const obj= Object.create(Object.prototype);
obj.__proto__ === Object.prototype; // true

const obj = Object.create(null);
obj.__proto__ === Object.prototype; // false;
console.log(obj.__proto__); // undefined

简易模拟 Object.create

function createObj(proto) {
    const F = function() {};
    F.prototype = proto;
    return new F();
}

原型链

定义

当在一个对象 obj 上访问某个属性时,若该属性不存在于 obj 上,则会通过 __proto__ 去对象的原型即 obj.__proto__ 上去找这个属性,若有则返回该属性,没有则继续去对象 obj 的原型的原型即 obj.__proto__.__proto__ 去找 ... 重复以上步骤,一直访问到 纯对象的原型Object.prototype,没有的话继续往上找即 Object.prototype.__proto__,即 null,此时直接返回 undefined

console.log(new Object().__proto__.__proto__); // null
Object.prototype.__proto__ === null; // null

这就推出了原型链之所以叫原型链而不叫原型环,说明它是有始有终的,原型链的顶层就是 null,返回 undefined,所以原型链不会无限的找下去

因此原型链可以描述为由对象的 __proto__ 属性将对象和原型联系起来直到Object.prototype.__proto__null 的链就是原型链

function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

const stu = new Student();
console.log(stu.gender); // => undefined

访问 stu.gender 的整个过程如下图: image.png

函数 Student 的原型链应该是这样的

image.png

上文介绍了 prototype__proto__ 的区别,其中原型对象 prototype 是构造函数的属性,__proto__ 是每个实例对象上都有的属性,这两个并不一样,但指向同个对象,如上面例子 stu.__proto__Student.prototype 指向同个对象

那原型链的构建是依赖于 prototype 还是 __proto__ 呢?

上图中,Student.prototype 中的 prototype 并没有构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 __proto__,如上图通过 stu.__proto__ 指向 Student.prototypestu.__proto__.__proto__ 指向 Object.prototyp,如此一层一层最终链接到 null

可以这么理解,Student 是一个 constructor 也是一个 function,它身上有着 prototype 的 reference,只要调用 stu = new Student() 就会将 stu.__proto__ 指向到 Student 的 prototype 对象

不要使用类似 Bar.prototype = Foo,因为这不会执行 Foo 的原型,而是指向函数 Foo。因此原型链将会回溯到 Function.prototype 而不是 Foo.prototype,因此 Foo 原型上的方法将不会在 Bar 的原型链上

function Foo() {
  return 'foo';
}
Foo.prototype.getMethod = function() { 
  return 'method';
}
function Bar() {
  return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数 Foo
const bar = new Bar();
console.dir(bar);
bar.method(); // bar.getMethod is not a function

bar.__proto__ === Foo.prototype; // false

原型链上属性的增删改查

通过一个对象改变了原型上的引用值类型的属性,则所有对象实例的这个属性值都会随之更改 image.png

依据当自身没有这个属性时就会向上往原型查询的说法,再次删除这个属性是不是就可以删除原型上的属性了?然而事实并没有,由此可见对象实例并不能删除原型上的属性

image.png

谁调用这个方法,这个方法中的 this 就指向这个调用它的对象

image.png

instanceof 操作符

平常判断一个变量的类型经常会使用 typeof 运算符,但对于引用类型来说并不能很好区分(除了函数对象会返回 function 外其他都返回 object

来看一下 MDN 上对于 instanceof 运算符的描述

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象实例的原型链中的任何位置

instanceoftypeof 非常的类似。instanceof 用于判断对象是否是某个构造函数的实例,若 obj instanceof A,就说明 objA 的实例 f

它的原理一句话概括就是:obj instanceof 构造器 A 等同于 判断 A 的 prototype 是不是 obj 的原型

instanceof 操作符左边是一个对象,右边是一个构造函数,在左边对象的原型链上查找(通过 __proto__)直到找到右边构造函数的 prototype 属性就返回 true,或查找到顶层 nullObject.prototype.__proto__),就返回 false

// 定义构造函数
function C(){} 
function D(){} 

const o1 = new C();
o1 instanceof C; // true,因为 Object.getPrototypeOf(o1) === C.prototype

o1 instanceof D; // false,因为 D.prototype 不在 o1 的原型链上

o1 instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o1) 返回 true
C.prototype instanceof Object; // true,同上

C.prototype = {};
const o2 = new C();

o2 instanceof C; // true

o1 instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o1 的原型链上

D.prototype = new C(); // 继承
const o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

typeof 方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型

简单模拟实现

// 第一种
// 参数 obj 表示 instanceof 左边的对象
// 参数 Constructor 表示 instanceof 右边的构造函数
function myInstanceOf(obj, Constructor) {
  // 取构造函数显示原型
  let rightP = Constructor.prototype; 
  // 取对象隐式原型
  let leftP = obj.__proto__; 
  // 到达原型链顶层还未找到则返回 false
  if (leftP === null) {
      return false;
  }
  // 对象实例的隐式原型等于构造函数显示原型则返回 true
  if (leftP === rightP) {
      return true;
  }
  // 递归查找原型链上一层
  return myInstanceOf(obj.__proto__, Constructor)
}

// 第二种
function myInstanceof(left, right) {
  let prototype = right.prototype;
  left = left.__proto__;
  while (true) {
    if (left === null) return false;
    if (prototype === left) return true;
    left = left.__proto__;
  }
}

现在就可以解释一些比较令人费解的结果了

let fn = function() {};
let arr = [];
fn instanceof Function; // true
fn instanceof Object; // true
// 1. fn.__proto__ === Function.prototype;
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;

arr instanceof Array; // true
arr instanceof Object; // true
// 1. arr.__proto__ === Array.prototype;
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype;

Object instanceof Object; // true
// 1. Object.__proto__ === Function.prototype;
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;

Function instanceof Function; // true
Function instanceof Object; // true
// 1. Function.__proto__ === Function.prototype;
// 2. Function.__proto__.__proto__ === Object.prototype;

Foo instanceof Function; // true 
Foo instanceof Foo; // false
// 1. Foo.__proto__ === Function.prototype;
// 2. Foo.__proto__.__proto__ === Function.prototype.__proto__ = Object.prototype;
// 3. Foo.__proto__.__proto__.__proto__ === Object.prototype.__proto__ = null;
// 4. null

总结:instanceof 运算符用于检查右边构造函数的 prototype 属性是否出现在左边对象的原型链中的任何位置,其实它表示的是一种原型链继承的关系

Object & Function

上面提到的 Object.__proto__ === Function.prototypeFunction.__proto__ === Function.prototype 到底是为什么呢?

Object.prototype

ECMAScript 上的定义

The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is "Object", and the initial value of the [[Extensible]] internal property is true Object.prototype 表示 Object 的原型对象,其 [[Prototype]] 属性是 null,访问器属性 __proto__ 暴露了一个对象的内部 [[Prototype]]Object.prototype 并不是通过 Object 函数创建的,为什么呢?看如下代码

function Foo() {
  this.value = 'foo';
}
let foo = new Foo();
foo.__proto__ === Foo.prototype; // true

实例对象的 __proto__ 指向构造函数的 prototype,即 foo.__proto__ 指向 Foo.prototype,但 Object.prototype.__proto__null,所以 Object.prototype 并不是通过 Object 函数创建的,那它如何生成的?其实 Object.prototype 是引擎根据 ECMAScript 规范创造的一个对象

所以可以说:所有实例都是对象,但是对象不一定都是实例

不考虑 null 的情况下,Object.prototype 就是原型链的顶端,所有对象实例都可以继承它的 toString 等方法和属性

Function.prototype

ECMAScript 上的定义

The Function prototype object is itself a Function object (its [[Class]] is "Function").

The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object.

The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object

Function.prototype 对象是一个函数(对象),其 [[Prototype]] 内部属性值指向内建对象 Object.prototypeFunction.prototype 对象自身没有 valueOf 属性,其从 Object.prototype 对象继承了 valueOf 属性

Function.prototype[[Class]] 属性是 Function,所以这是一个函数,但又不大一样。为什么这么说呢?因为只有函数才有 prototype 属性,但并不是所有函数都有这个属性,因为 Function.prototype 这个函数就没有

Function.prototype
// ƒ () { [native code] }

Function.prototype.prototype
// undefined

下面这个函数也没有 prototype 属性

let fun = Function.prototype.bind();
// ƒ () { [native code] }

fun.prototype
// undefined

为什么没有呢?我的理解是 Function.prototype 是引擎创建出来的函数,引擎认为不需要给这个函数对象添加 prototype 属性,不然 Function.prototype.prototype… 将无休无止并且没有存在的意义

Function.prototype 不可写、不可配置、不可遍历,即它永远指向固定的一个对象且是其他所有函数的原型对象,所有函数本身的 __proto__ 指向它

引擎首先创建了 Object.prototype ,然后创建了 Function.prototype 并且通过 __proto__ 将两者联系了起来

Object

JS 中 ObejctFunction 都是构造函数(构造函数也是函数),和 objectfunction 不是一个东西,分别用于创建 对象函数 实例

ECMAScript 上的定义

The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object

Object 作为构造函数时,其 [[Prototype]] 内部属性值指向 Function.prototype,即

Object.__proto__ === Function.prototype; // true

Object 的全貌是:function Object() { ... },它是普通对象的构造函数,当 var foo = {} 时相当于实例化 Object,即 new Object()

使用 new Object() 创建新对象时,这个新对象的 [[Prototype]] 内部属性指向构造函数的 prototype 属性,对应就是 Object.prototype

当然也可以通过对象字面量等方式创建对象

  • 使用对象字面量创建的对象,其 [[Prototype]] 值是 Object.prototype
  • 使用数组字面量创建的对象,其 [[Prototype]] 值是 Array.prototype
  • 使用 function f(){} 函数创建的对象,其 [[Prototype]] 值是 Function.prototype
  • 使用 new fun() 创建的对象,其中 fun 是由 JavaScript 提供的内建构造器函数之一(Object, Function, Array, Boolean, Date, Number, String 等),其 [[Prototype]] 值是 fun.prototype
  • 使用其他 JavaScript 构造器函数创建的对象,其 [[Prototype]] 值就是该构造器函数的 prototype 属性
// 原型链:o.__proto__ -> Object.prototype -> null
let o = {a: 1};

// 原型链:a -> Array.prototype -> Object.prototype -> null
let a = ["yo", "whadup", "?"];

// 原型链:f -> Function.prototype -> Object.prototype -> null
function f(){
  return 1;
}

// 原型链:fun -> Function.prototype -> Object.prototype -> null
let fun = new Function();

// 原型链:foo -> Foo.prototype -> Object.prototype -> null
function Foo() {}
let foo = new Foo();

// 原型链:foo -> Object.prototype -> null
function Foo() {
  return {};
}
let foo = new Foo();

Function

ECMAScript 上的定义

The Function constructor is itself a Function object and its [[Class]] is "Function". The value of the [[Prototype]] internal property of the Function constructor is the standard built-in Function prototype object

Function 构造函数是一个函数对象,其 [[Class]] 属性是 FunctionFunction[[Prototype]] 属性指向了 Function.prototype,即

Function.__proto__ === Function.prototype; // true

Function 的全貌是:function Function() { ... },它是函数对象的构造函数,当 function foo() {} 时相当于实例化 Function,即 new Function()

我们知道函数的本质是通过 new Function() 生成的,但Function.prototype 是引擎自己创建的,所以又可以得出一个结论

不是所有函数都是 new Function() 产生的

Function & Object 鸡蛋问题

先看下面代码

Object instanceof Function; // true
Object.__proto__ === Function.prototype; // true

Function instanceof Object; // true
Function.__proto__.__proto__ === Object.prototype; // true

Object instanceof Object; // true
Object.__proto__.__proto__ === Object.prototype; // true

Function instanceof Function; // true
Function.__proto__ === Function.prototype; // true

Object 构造函数继承了 Function.prototype,一切函数对象都直接继承自 Function 对象(系统内置的构造函数),函数对象 包括了 FunctionObjectArrayStringNumberRegExpDate 等,Function 其实不仅用于构造函数,它也充当了 函数对象 的构造器

同时 Function 构造函数继承了 Object.prototype,这里就产生了 鸡和蛋 的问题。因为 Function.prototypeFunction.__proto__ 都指向 Function.prototype

对于 Function.__proto__ === Function.prototype 这一现象有 2 种解释,争论点在于 Function 对象是不是由 Function 构造函数创建的一个实例?

  • YES:按照 JavaScript 中实例的定义,ab 的实例即 a instanceof btrue,默认判断条件就是 b.prototypea 的原型链上。而 Function instanceof Functiontrue,本质上即 Object.getPrototypeOf(Function) === Function.prototype,正符合此定义
  • NOFunctionbuilt-in 的对象,即并不存在 Function 对象由 Function 构造函数创建 这样显然会造成鸡生蛋蛋生鸡的问题。实际上当直接写一个函数时(如 function f() {}x => x),也不存在调用 Function 构造器,只有在显式调用 Function 构造器时(如 new Function('x', 'return x'))才有

个人偏向于第二种解释,即先有 Function.prototype 然后有 function Function(),所以就不存在鸡生蛋蛋生鸡问题了,把 Function.__proto__ 指向 Function.prototype,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype,且 Function 本质也是一个函数对象,事实上 Function 只是一个祖先、一个构造函数,并不是一个实例出来的函数对象,所以本来没必要拥有 __proto__ 这个属性,但这样的话会显得 Function 很另类,于是也给它加上属性 __proto__ 并指向 Function.prototype ,只是为了表明 Function 作为一个原生构造函数,本身也是一个函数对象,而且这也保证了原型链的完整,让 Function 可以获取定义在 Object.prototype 上的方法

一切函数对象(包括 Object 对象) 都直接继承自 Function 对象,Function 对象直接继承自己,最终继承自 Object 对象,Object 和 Function 是互相继承的关系

有了 Function.prototype 以后才有了 function Function(),然后其他的构造函数都是 function Function() 生成的

一切对象都继承自 Object.prototype,而一切函数对象都继承自 Function.prototype (Function.prototype 最终继承自 Object.prototype),即普通对象和函数对象的区别是:普通对象直接继承了 Object.prototype,而函数对象在中间还继承了 Function.prototype

因此可以得出以下总结:

  1. Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  2. Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  3. 所有通过字面量表示法创建的普通对象的构造函数为 Object
  4. 所有原型对象都是普通对象,构造函数为 Object
  5. 所有函数的构造函数是 Function
  6. Function.prototypeObject.prototype 没有原型对象
  7. Function.prototypeObject.prototype 是两个由引擎创建出来的特殊对象,除了这两个特殊对象,其他对象都是通过构造器 new 出来的
  8. 函数的 prototype 是一个对象,即原型。对象的 __proto__ 指向原型,__proto__ 将对象和原型连接起来组成了原型链
  9. Function.prototype.__proto__ === Object.prototype
  10. Object.__proto__ === Function.prototype
  11. Function.__proto__ === Function.prototype
  12. Function.__proto__ === Object.__proto__
  13. Object.prototype.__proto__ === null
  14. Object => Function.prototype => Object.prototype => null
  15. Function => Function.prototype => Object.prototype => null
  16. 若是自定义的构造函数,形成的原型链如下:Foo => Function.prototype => Object.prototype => null
  17. 通过自定义构造函数实例化的对象,形成的原型链如下:obj => Foo.prototype => Object.prototype => null

image.png

image.png

内置类型构建过程

JavaScript 内置类型是浏览器内核自带的,浏览器底层对 JavaScript 的实现基于 C/C++,那么浏览器在初始化 JavaScript 环境时都发生了什么?

  • 用 C/C++ 构造内部数据结构创建一个 OP 即 (Object.prototype) 以及初始化其内部属性但不包括行为
  • 用 C/C++ 构造内部数据结构创建一个 FP 即 (Function.prototype) 以及初始化其内部属性但不包括行为
  • 将 FP 的 [[Prototype]] 指向 OP
  • 用 C/C++ 构造内部数据结构创建各种内置引用类型
  • 将各内置引用类型的[[Prototype]]指向 FP
  • 将 Function 的 prototype 指向 FP
  • 将 Object 的 prototype 指向 OP
  • 用 Function 实例化出 OP、FP,以及 Object 的行为并挂载
  • 用 Object 实例化出除 Object 以及 Function 的其他内置引用类型的 prototype 属性对象
  • 用 Function 实例化出除 Object 以及 Function 的其他内置引用类型的 prototype 属性对象的行为并挂载
  • 实例化内置对象 Math 以及 Grobal 至此所有内置类型构建完成

原型污染

曾经 lodash 爆出了一个严重的安全漏洞:Lodash 库爆出严重安全漏洞,波及 400 万+项目,这个安全漏洞就是由于原型污染导致的,Lodash 库中的函数“defaultsDeep”很有可能会被欺骗添加或修改 Object.prototype 的属性,最终可能导致 Web 应用程序崩溃或改变其行为,具体取决于受影响的用例

虽然说任何一个原型被污染了都有可能导致问题,但一般提原型污染说的就是 Object.prototype 被污染

原型污染的危害

  • 性能问题:原型被污染会增加遍历的次数,每次访问对象自身不存在的属性时也要访问下原型上被污染的属性
  • 导致意外的逻辑 bug:看下面这个从别的大佬的文章中看到的例子
'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');

const isObject = (obj) => obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a;
}

function clone(a) {
  return merge({}, a);
}

// Constants
const PORT = 8080;
const HOST = '127.0.0.1';
const admin = {};

// App
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());

app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
  var body = JSON.parse(JSON.stringify(req.body));
  var copybody = clone(body);
  if (copybody.name) {
    res.cookie('name', copybody.name).json({
      done: 'cookie set',
    });
  } else {
    res.json({
      error: 'cookie not set',
    });
  }
});
app.get('/getFlag', (req, res) => {
  var аdmin = JSON.parse(JSON.stringify(req.cookies));
  if (admin.аdmin == 1) {
    res.send('hackim19{}');
  } else {
    res.send('You are not authorized');
  }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

这段代码的漏洞就在于 merge 函数上,可以这样攻击:

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://127.0.0.1:4000/signup';

curl -vv 'http://127.0.0.1/getFlag'

首先请求 /signup 接口,在 NodeJS 服务中,我们调用了有漏洞的 merge 方法,并通过 __proto__Object.prototype(因为 {}.__proto__ === Object.prototype) 添加上一个新的属性 admin 且值为 1

再次请求 getFlag 接口访问了 Object 原型上的 admin,条件语句 admin.аdmin == 1true,服务被攻击

预防原型污染

其实原型污染大多发生在调用会修改或扩展对象属性的函数时,如 lodash 的 defaults、jQuery 的 extend,预防原型污染最主要还是要有防患意识,养成良好的编码习惯

Object.create(null)

  • Object.create(null) 创建没有原型的对象,即便对它设置 __proto__ 也没有用,因为它的原型一开始就是 null,没有 __proro__setter

image.png

Object.freeze(obj) 可以通过 Object.freeze(obj) 冻结对象 obj,被冻结的对象不能被修改属性,成为不可扩展对象。不能修改不可扩展对象的原型,否则会抛 TypeError:

const obj = Object.freeze({ name: 'xiaoHong' });
obj.xxx = 666;
console.log(obj); // => { name: 'xiaoHong' }
console.log(Object.isExtensible(obj)); // => false
obj.__proto__ = null; // => TypeError: #<Object> is not extensible

关于原型污染可阅读:最新:Lodash 严重安全漏洞背后你不得不知道的 JavaScript 知识

继承

原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层的组成了原型链
原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层往上找,说白了就是一个对象可以访问其他对象的属性
继承存在的意义就是属性共享:好处一是代码重用(字面意思);好处二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性

ES5 继承实现方式

原型链继承

将父类的实例作为子类的原型

function Parent() {
     this.name = 'tn';
}
Parent.prototype.getName = function () {
    console.log(this.name);
}
function Son () {};
// 关键,创建 Parent 的实例并将该实例赋值给 Son.prototype
Son.prototype = new Parent();
const son1 = new Son();
console.log(son1.getName());  // tn 

// 缺点
function Parent () {
    this.names = ['licy', 'tn'];
}
function Son () {}
Son.prototype = new Parent();
const son1 = new Son();
son1.names.push('yayu');
console.log(son1.names); // ["licy", "tn", "yayu"]

const son2 = new Son();
console.log(son2.names); // ["licy", "tn", "yayu"]

image.png

  • 优点:父类方法可以复用,父类的属性与方法子类都能访问
  • 缺点:
    • 父类的引用属性会被所有子类实例共享,子类会继承过多没有用的属性,造成大量的浪费且多个实例对引用类型的操作会被篡改
    • 由于子类实现的继承是靠其原型 prototype 对父类进行实例化实现的,因此在构建子类实例时是无法向父类传递参数的,因而在实例化父类时也无法对父类构造函数内的属性进行初始化

借用构造函数继承

使用父类的构造函数来增强子类实例,利用 callapply 可改变 this 指向的特点,将父类构造函数内容复制给子类构造函数,由于父类中给 this 绑定属性,因此子类自然也就继承父类的共有属性,这是所有继承中唯一不涉及到 prototype 的继承

function Parent (name) {
    this.books = ['js','css'];
    this.name = name;
}
Parent.prototype.showBooks = function() {
  console.log(this.books);
}

function Son (name) {
    Parent.call(this, name);
}
const son1 = new Son('tn');
console.log(son1.name); // tn
son1.showBooks(); // TypeError: son1.showBooks is not a function

const son2 = new Son('licy');
console.log(son2.name); // licy

function SuperType(){ 
   this.color=["red","green","blue"]; 
}
function SubType(){ 
   //继承自 SuperType 
   SuperType.call(this);
}
const instance1 = new SubType(); 
instance1.color.push("black"); 
console.log(instance1.color); //["red", "green", "blue", "black"] 

const instance2 = new SubType(); 
console.log(instance2.color); //["red", "green", "blue"]
  • 优点:
    • 父类的引用属性不会被共享,避免了引用类型的属性被所有实例共享且避免了多个实例对引用类型的操作会被篡改的问题
    • 子类构建实例时可以向父类传递参数
  • 缺点
    • 不能继承父类原型上的属性/方法(若原型上的属性/方法想被子类继承,就必须放到构造函数中)
    • 父类的方法和属性不能复用,子类实例的方法每次都是单独创建的,这样就违背了代码复用的原则
    • 每个子类都有父类实例函数的副本,影响性能

组合继承

原型链继承借用构造函数继承 结合

用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"];
}    
SuperType.prototype.sayName = function(){ console.log(this.name); }; 
function SubType(name, age){ 
    // 继承属性
    // 第二次调用 SuperType() 
    // 第二次又给子类的构造函数添加了父类的 name, colors 属性
    // 使用子类创建的实例对象上的同名属性覆盖了子类原型中的同名属性,这造成了性能浪费
    SuperType.call(this, name); 
    this.age = age; 
} 
// 继承方法,构建原型链 
// 第一次调用 SuperType() 
// 第一次给子类的原型添加了父类的 name, colors 属性
SubType.prototype = new SuperType(); 

// 重写 SubType.prototype 的 constructor 属性,指向自己的构造函数 SubType 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){ alert(this.age); }; 

const instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //"Nicholas"; 
instance1.sayAge(); //29 

const instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //"Greg"; 
instance2.sayAge(); //27
  • 优点:
    • 父类的方法可以被复用
    • 父类的引用属性不会被共享
    • 子类构建实例时可以向父类传递参数
    • 可以继承父类的属性和方法,同时也可以继承原型的属性和方法
  • 缺点:
    • 使用子类创建实例对象时,父类调用了两次,因此产生了两份实例,其原型中会存在两份相同的属性/方法

原型式继承

ES5 Object.create 的模拟实现,利用一个空对象作为中介,将传入的对象作为该空对象构造函数的原型,object() 对传入的对象执行了一次浅复制

function object(obj){
  // 声明一个过渡对象 
  function F(){};
  // 过渡对象的原型继承传入的对象
  F.prototype = obj;
  // 返回过渡对象的实例
  return new F();
}

const person = { 
  name: "Nicholas", 
  friends: ["Shelby", "Court", "Van"] 
};

const anotherPerson = object(person); 
const yetAnotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

console.log(anotherPerson.name) // "Greg"
console.log(yetAnotherPerson.name) // "Nicholas"
console.log(yetAnotherPerson.friends) //["Shelby", "Court", "Van", "Rob"]
  • 缺点:
    • 父类的引用类型的属性值会被所有子类实例共享,改动一个会影响另一个,这点跟原型链继承一样
    • 子类构建实例时不能向父类传递参数
  • ES5 中 Object.create() 的方法能够代替上面的 object方法,Object.create() 方法规范化了原型式继承

寄生式继承

  • 在原型式继承的基础上,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来增强对象,最后返回对象
  • 这样新创建的对象不仅仅有父类的属性和方法,还可新增了别的属性和方法
function createAnother(original){ 
   const clone = object(original); // 通过调用 object() 函数创建一个新对象 
   // 或
   const clone = Object.create(o);
   clone.sayHi = function(){ 
       // 以某种方式来增强对象 
       alert("hi"); 
   }
   return clone; // 返回这个对象 
}   
const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
  • 缺点(同原型式继承)

寄生组合式继承

寄生组合式继承是寄生式继承和借用构造函数继承的组合,只调用了一次父类构造函数,解决了组合继承有会两次调用父类的构造函数造成浪费的缺点

function object(o) {
    //声明一个过渡对象
    function F() {}
    //过渡对象的原型继承父对象
    F.prototype = o;
    //返回过渡对象的实例,该对象的原型继承了父对象
    return new F();
}
function prototype(child, parent) {
    // 复制一份父类的原型副本到变量中
    const prototype = object(parent.prototype);
    // 增强对象,修正因为重写子类的原型导致子类的 `constructor` 属性被修改
    prototype.constructor = child;
     // 设置子类原型
    child.prototype = prototype;
}
// 使用时
prototype(Child, Parent);

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时原型链还能保持不变,因此能够正常使用 instanceofisPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,也是现在很多库实现的方法

封装

function inherit (Target, Origin) {
    // 声明一个过渡对象
    function F () {};
    // 过渡对象的原型继承父对象,创建了父类原型的浅复制
    F.prototype = Origin.prototype;
    // 返回过渡对象的实例,该对象的原型继承了父对象
    Target.prototype = new F();
    // 修正子类原型的构造函数
    Target.prototype.constructor = Target;
    // 无法知道自己真正继承至谁(记住最好,也不强求)
    // 为了保存一下它的父类,也用一个 uber 来记录一下父类
    // 因为 super 是保留字不能使用,所以使用了 uber
    Target.prototype.uber = Origin.prototype; 
}    

雅虎的高端写法,采用闭包的私有化变量

var inherit = (function () {
    var F = function () {};
    return function (Target, Origin) {
        F.prototype = Origin.prototype;
        Target.prototype = new F();
        Target.prototype.constructor = Target;
        Target.prototype.uber = Origin.prototype;
    }   
}());      

混入方式继承多个对象

Object.assign 会把 OtherSuperClass 原型上的方法属性拷贝到 MyClass 原型上,使 MyClass 的所有实例都可使用 OtherSuperClass 上的方法

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do something
};

ES6 类继承 extends

ES6 的继承和寄生组合继承相似,本质上 ES6 继承是 ES5 继承的一种语法糖extends 关键字主要用于类声明或类表达式中,以创建一个类表示该类是另外某个类的子类

constructor 表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError 错误,若没有显式指定构造方法,则会添加默认的 constructor 方法,例子如下

class Rectangle { 
   // constructor 
   constructor(height, width) { 
      this.height = height; this.width = width;
   } 
   // Getter 
   get area() { return this.calcArea() } 
   // Method 
   calcArea() { return this.height * this.width; } 
}      
const rectangle = new Rectangle(10, 20); 
console.log(rectangle.area); // 输出 200 

// 继承 
class Square extends Rectangle { 
   constructor(length) { 
      super(length, length); // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。 
      this.name = 'Square'; 
   }    
   get area() { return this.height * this.width; } 
}   
const square = new Square(10); 
console.log(square.area); // 输出 100

extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式类似

// extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式相似
function _inherits(subType, superType) { 
    // 创建对象,创建父类原型的一个副本 
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性 
    // 指定对象,将新创建的对象赋值给子类的原型 
    subType.prototype = Object.create(superType && superType.prototype, { 
        constructor: { 
            value: subType, 
            enumerable: false, 
            writable: true, 
            configurable: true 
        } 
    }); 
   
    if (superType) { 
        Object.setPrototypeOf ? 
        Object.setPrototypeOf(subType, superType) : 
        subType.__proto__ = superType; 
    } 
}   

总结

  • 函数声明和类声明的区别:函数声明会提升,类声明不会。首先需要声明类然后访问它,否则像下面的代码会抛出一个 ReferenceError
    let p = new Rectangle(); 
    // ReferenceError
    class Rectangle {}
    
  • ES6 Class extends 是 ES5 继承的语法糖
  • ES5 继承和 ES6 继承的区别
    • ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this

      Child.prototype = new Parent() || Parent.apply(this) || Parent.call(this)
      
    • ES6 的继承有所不同,在 ES6 class 中,实质上是先创建父类的实例对象 this,然后再用子类的构造函数修改 this。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象而是继承父类的 this 对象,然后对其进行加工

扩展

一道关于原型的题目

function Page() {
  return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];

const p1 = new Page();
const p2 = Page();

console.log(p1.hosts);  // undefined
console.log(p2.hosts); // Uncaught TypeError: Cannot read property 'hosts' of undefined

原因分析

  • 之前文章提过 new 时若 return 了对象,则会直接拿这个对象作为 new 的结果,因此 p1 应该是 this.hosts 的结果,而在 new Page() 时,this 是一个以 Page.prototype 为原型的 target 对象,所以这里 this.hosts 可以访问到 Page.prototype.hosts['h2']。因此 p1 就是等于 ['h2']['h2'] 没有 hosts 属性所以返回 undefined
  • console.log(p2.hosts) 会报错是因为 p2 是直接调用 Page 构造函数,这个时候 this 指向全局对象,全局对象并没 hosts 属性,因此返回 undefined,往 undefined 上访问 hosts 当然报错