【学习笔记】JavaScript - 从新认识构造函数、原型、原型链、继承

前言

构造函数

什么是构造函数

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

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

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

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()"markdown

Symbol 是基本数据类型,但做为构造函数来讲它并不完整,由于它不支持语法 new Symbol(),所以认为其不是构造函数,若要生成实例直接使用 Symbol() 便可(来自 MDN),每一个从 Symbol() 返回的值都是惟一的cookie

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 就是典型的函数对象

下述代码中 obj一、obj二、obj三、obj4 都是普通对象,fun一、fun二、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__

一、首先须要明确:__proto__constructor 是对象独有的;prototype 是函数独有的
二、但在 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 固然报错
相关文章
相关标签/搜索