对象的几种创建方式
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 个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
缺点:使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍
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是实例,p是obj3的原型( 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)主要完成了三件事情:
- 创建一个对象
- 继承指定父对象,将对象的
__proto__指向父对象。 - 为新对象扩展新属性
何时使用 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 重新指向本身
存在的问题:
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数
- 创建多个实例的时候,因为共享原型链的问题,一个实例修改原型链上的属性会影响到其他实例获取属性。
- 子类原型链上定义的方法有先后顺序问题,因为子类原型链会被重新指向。
- 需要修复
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”
优点:
- 构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数.
缺点:
- 就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
- 这种方式,虽然改变了
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 类的内部定义的所有方法,都是不可枚举的
ES6的class类必须用new命令操作,而ES5的构造函数不用new也可以执行。ES6的class类不存在变量提升,必须先定义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 原理
- new 关键字会首先创建一个空对象
- 将这个空对象的原型对象指向构造函数的原型属性,从而继承原型上的方法
- 将 this 指向这个空对象,执行构造函数中的代码,以获取私有属性
- 如果构造函数返回了一个对象 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;
}