对象的几种创建方式

1. 字面量

// 字面量的方式。
const obj11 = { name: "yiliang" };
// 内置对象(内置的构造函数)
const obj12 = new Object({ name: `yiliang` });
// 上面的两种写法,效果是一样的。因为,第一种写法,`obj11` 会指向 `Object`。

2. 构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    alert(this.name);
  };
}
const person1 = new Person("Nicholas", 29, "Software Engineer");

以这种方式调用构造函数实际上会经历以下 4 个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

缺点:使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍

3. 原型

不必在构造函数中定义对象实例的信息,将这些信息直接添加到原型对象

function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};
const person1 = new Person();
person1.sayName(); //"Nicholas"
const person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

缺点:实例没有属于自己的全部属性的

4. Object.create

obj3是实例,pobj3的原型( name是 p 原型里的属性),构造函数是Object

const p = { name: "happy" };
//此方法创建的对象,是用原型链连接的
const obj3 = Object.create(p);
// obj3.__proto__ === p

传入一个原型对象,创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__,实现继承。

Object.create(null) 生成的对象里面没有任何属性,非常“空”,我们称它为字典,这种字典对象适合存放数据,不必担心原型带来的副作用。这也是 Vue 中为什么很多地方都使用了 Object.create(null) 的原因。

实现 Object.create

Object.create(parent)主要完成了三件事情:

  1. 创建一个对象
  2. 继承指定父对象,将对象的 __proto__ 指向父对象。
  3. 为新对象扩展新属性

何时使用 create: 希望在创建对象时就提前指定继承的父对象,并同时扩展新属性时。

Object.myCreate = function (parent, props) {
  const obj = new Object();
  Object.setPrototypeOf(obj, parent);
  Object.defineProperties(obj, props);
  return obj;
};

类与继承

继承的本质就是原型链

JS 继承的 6 种方法

1. 原型链继承

实现的本质是重写原型对象

function Animal(species) {
  this.species = species;
}

function Cat() {}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 修复: 将 Cat.prototype.constructor 重新指向本身

存在的问题:

  1. 在创建子类型的实例时,不能向超类型的构造函数中传递参数
  2. 创建多个实例的时候,因为共享原型链的问题,一个实例修改原型链上的属性会影响到其他实例获取属性。
  3. 子类原型链上定义的方法有先后顺序问题,因为子类原型链会被重新指向。
  4. 需要修复prototype.constructor指向问题

2. 借用构造函数继承

在子类型构造函数的内部调用父类的构造函数

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
  // 继承 SuperType.【重要】此处用 call 或 apply 都行:改变 this 的指向
  // 让 Parent 的构造函数在 child 的构造函数中执行.
  SuperType.call(this);
}

const instance1 = new SubType();
const instance2 = new SubType();
instance1.colors.push("black");

console.log(instance1.colors); //"red,blue,green,black"
console.log(instance2.colors); //"red,blue,green”

优点:

  1. 构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数.

缺点:

  1. 就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
  2. 这种方式,虽然改变了 this 的指向,但是,子类无法继承父类的原型

3. 组合继承(原型+借用构造)

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

//
SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  //继承属性
  SuperType.call(this, name);
  this.age = age;
}

//继承方法

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

const instance1 = new SubType("Nicholas", 29);

instance1.colors.push("black");
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

const instance2 = new SubType("Greg", 27);

instance2.sayName(); //"Greg";
instance2.sayAge(); //27

console.log(instance2.colors); //"red,blue,green"
console.log(instance1.colors); //"red,blue,green,black"

先借用借用构造函数,再通过原型链挂载,最后修改原型的 constructor。这种方式,能解决之前两种方式的问题:既可以继承父类原型的内容,也不会造成原型里属性的修改。

缺点:让父亲Parent的构造方法执行了两次。

4. 寄生继承

改进了组合继承的缺点,只需要调用 1 次父类的构造函数。它是引用类型最理想的继承范式

/**
 * 寄生组合继承的核心代码
 * @param {Function} Sub 子类
 * @param {Function} Parent 父类
 */
function inheritPrototype(Sub, Parent) {
  // 拿到父类的原型
  let prototype = Object.create(Parent.prototype);
  // 改变 constructor 指向
  prototype.constructor = Sub;
  // 父类原型赋给子类
  Sub.prototype = prototype;
}

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function () {
  console.log("Animal");
};

function Cat() {
  Animal.apply(this, arguments); // 只调用了 1 次构造函数
}

inheritPrototype(Cat, Animal);

5. 拷贝继承

如果把父对象的所有属性和方法,拷贝进子对象

function extend(Child, Parent) {
  let p = Parent.prototype;
  let c = Child.prototype;
  for (let i in p) {
    c[i] = p[i];
  }
  c.uber = p;
}

6. Class 继承

class Parent {
  constructor(value) {
    this.val = value;
  }
  getValue() {
    console.log(this.val);
  }
}
class Child extends Parent {
  constructor(value) {
    // 在子类构造函数中必须调用 `super`,因为这段代码可以看成 `Parent.call(this, value)`。
    super(value);
    this.val = value;
  }
}
let child = new Child(1);
child.getValue(); // 1
child instanceof Parent; // true
Class 和 ES5 构造函数有什么不同点?

Class 类的内部定义的所有方法,都是不可枚举的

  • ES6class类必须用new命令操作,而ES5的构造函数不用new也可以执行。
  • ES6class类不存在变量提升,必须先定义class之后才能实例化,不像ES5中可以将构造函数写在实例化之后。
  • ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

原型继承重新赋值的原因

function Foo() {
  this.value = 42;
}
Foo.prototype = {
  method: function () {
    return true;
  },
};
function Bar() {
  var value = 1;
  // 手动指定返回的话,就只是一个默认的对象了,而丢掉了继承中间的关系
  return {
    method: function () {
      return value;
    },
  };
}
Foo.prototype = new Bar();
// 构造函数 Bar 返回的是手动指定的一个对象,所以构造函数是 Object
console.log(Foo.prototype.constructor); // ƒ Object() { [native code] } 指向内置的 Object() 方法
// instanceof 自然也不成立了
console.log(Foo.prototype instanceof Bar); // false
var test = new Foo();
console.log(test instanceof Foo); // true
console.log(test instanceof Bar); // false
console.log(test.method()); // 1

Foo.prototype 的 constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的.prototype 对象引用,那么新对象并不会自动获得 constructor 属性。(这也是原型继承时,需要重新赋值的原因。) Foo.prototype 没有 constructor 属性,所以他会委托[[Prototype]]链上的委托连顶端的 Object.prototype。这个对象有 constructor 属性,指向内置的 Object(...)函数。

new 原理

  1. new 关键字会首先创建一个空对象
  2. 将这个空对象的原型对象指向构造函数的原型属性,从而继承原型上的方法
  3. 将 this 指向这个空对象,执行构造函数中的代码,以获取私有属性
  4. 如果构造函数返回了一个对象 res,就将该返回值 res 返回,如果返回值不是对象,就将创建的对象返回

模拟 new 实现:

// 因为 new 是关键字,我用函数的形式来实现,可以将构造函数和构造函数的参数传入
function myNew(Fn, ...args) {
  // 1. 创建一个空对象,并将对象的 __proto__ 指向构造函数的 prototype 这里我两步一起做了
  const obj = Object.create(Fn.prototype);
  // const obj = {}
  // obj.__proto__ = Fn.prototype
  // 2. 将构造函数中的 this 指向 obj,执行构造函数代码,获取返回值
  const res = Fn.apply(obj, args);
  // 3. 判断返回值类型
  return res instanceof Object ? res : obj;
}

构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?

  • 没有自己的 this,无法调用 call,apply
  • 没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 __proto__

封装一个原生的继承方法

function extendsClass(Parent, Child) {
  function F() {}
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  return Child;
}
Last Updated:
Contributors: yiliang114