JS 中的 "类" 是通过 "构造函数 + 原型链" 的方式实现的。其中构造函数用于生成实例对象,原型链用于实现对象之间的继承关系。
继承是什么意思?A对象通过继承B对象,就能直接拥有B对象的所有属性和方法,实现代码的复用。
构造函数
- 类是一个模板,可以通过类创建多个实例。
- 对象是通过类 new 出来的实例,是单个实物的抽象,人和小明的关系。
- 构造函数用于生成实例对象。
let Vehicle = function() { // 1.函数名首字母大写
this.price = price; // 2.使用 this 指代要生成的实例对象
}
let v1 = new Vehicle(); // 3.使用 new
对于构造函数 new 生成的多个实例对象,他们之间并不能共享属性。因此相同的方法需要生成多次,造成系统资源的浪费,需要通过原型对象解决。因为原型对象上的所有属性和方法,都能被实例对象共享。
一、原型对象
JS 规定,每个函数都有一个 prototype 属性,指向一个对象,这个对象就是该函数的 原型对象。 对于构造函数来说,使用 new 创建实例对象时,构造函数的 prototype 属性会自动成为该实例的原型对象 { 该实例对象中会存在一个私有属性(一般用__proto__ 或 proto 表示)来指向构造函数的原型对象},当实例对象本身没有某个属性或方法时,就会到原型对象中寻找。
另外需要注意的是,原型对象中也默认会有一个属性 constructor 指向它的构造函数。(原型对象中还有一个 proto 属性指向上一级的原型对象)。
二、与原型有关的属性和方法
2.1 prototype 属性
prototype存在于构造函数中,它指向了这个构造函数的原型对象。
补充:prototype 存在于任意函数中,但我们只关注构造函数。
2.2 constructor 属性
constructor 存在于原型对象中,它指向了构造函数。
由于存在于原型对象,所以会被实例对象继承,以确定是哪一个构造函数产生的。
2.3 __proto__ 属性
在构造函数 new 生成的实例中,默认有一个不可访问的属性 __proto__ 指向了构造函数的原型对象。 这也是与原型链相关的属性,就是通过该属性去层层向上查找原型对象的。
2.4 prototype 和 __proto__区别
一个在构造函数中,一个存在于对象中。都指向构造函数的 prototype。
三、原型链
3.1 原型链
当我们访问一个实例对象的属性时,如果这个对象内部不存在该属性(如果赋值 undefined,则表示该属性存在),那么就会去它的原型对象中找(__proto__ 指向的),找不到就再向上查找,最终上溯到 Object.prototype、null,都找不到就返回undefined。这些原型对象形成的就是一个“原型链”。
Object.prototype 对象的原型是 null,null 没有任何属性和方法,也没有自己的原型。因此原型链的尽头就是 null。
(??待阅读全文核实,原型链是__proto__实现的。)
// 获取对象的原型
Object.getPrototypeOf(Object.prototype); // null
3.2 Function.prototype 和 Object.prototype
- 构造函数的原型都是
Function.prototype,函数的原型也是Function.prototype。 所以单从构造器角度来说,Funtion.prototype 扮演了创世主的角色。
Object.getPrototypeOf(Object) === Function.prototype // true,查找原型的方式 1
Object.__proto__ === Function.prototype // true,查找原型的方式 2
Object.constructor = Function // true
Object.getPrototypeOf(Function) === Function.prototype // true
Function.__proto__ === Function.prototype // true
Function.constructor == Function // true
var fn = function() {}
fn.__proto__ === Function.prototype //true
fn.constructor === Function //true
注意,区分构造器做实例对象(上面) 和 构造器做构造函数(下面) 的两种情况!
2.
Function.prototype 的类型是Function! 并不是对象!所以并不是所有的 prototype 属性都是 Object 类型。
- 站在构造器角度,Function.prototype 起源更早;站在原型链角度,Object.prototype 起源更早。
四、继承的方式
4.1 原型链继承
原理:将父类的实例作为子类的原型,这样可以继承 「父类构造函数和父类原型」 的属性和方法。
- 为什么会继承父类构造函数?:new 的实例对象会继承构造函数的属性和方法,但属性或方法不能在多个实例对象之间共享。
- 为什么会继承父类原型?:实例对象会继承原型对象的属性和方法,并在多个实例对象之间共享。
// 注意:类是由构造函数和原型对象实现的
function Parent() { // 父类
this.name = 'Jack';
}
Parent.prototype.getName = function() { // 父类原型
return this.name;
}
function Child() {}
Child.prototype = new Parent(); // 子类原型等于父类实例
Child.prototype.constructor = Child; // constructor也一起绑定
let child1 = new Child();
console.log(child1.getName()); // Jack
优点:简单易实现,父类构造函数和父类原型中的属性和方法都能访问到。
缺点:(1)父类中引用类型的属性会被所有子类实例共享。(2)创建子类实例时不能向父类构造函数传参。
4.2 借用构造函数继承
原理:在子类的构造函数中执行父类的构造函数,并绑定子类的 this。
function Parent(name) { // 父类,实现传参
this.name = name;
}
Parent.prototype.getName = function() { // 父类原型
return this.name;
}
function Child() {
Parent.call(this, 'Jack');
}
优点:解决了原型链继承中父类引用类型属性共享的问题,而且可以向父类构造函数传参。
缺点:因为不能继承父类原型上的方法,所有方法都在父类构造函数中定义,每次创建实例都会创建一遍方法。
4.3 原型链 + 借用构造函数的"组合继承"
是 JS 最常用的继承方式。
// 相比原型链继承的区别
function Parent(name) { // 1.传参
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {
Parent.call(this, 'Jack'); // 2.执行父类构造
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
优点:融合了两种继承方式的优点。
缺点:每次创建子类实例,都会执行两次父类构造、生成两个实例。
4.4 原型式继承
将实例对象作为原型。缺点和原型链继承一样,子类实例会共享父类中引用类型的属性值。
实际就是Object.create()的模拟实现。
function createObj(obj) {
function F(){} // 创建一个空的构造函数
F.prototype = obj; // 让它的 prototype 属性指向参数对象 obj
return new F(); //
}
let person = {
name : 'Jack'
};
let p1 = createObj(person);
4.5 寄生组合继承
原理:将指向父类实例 改为 指向父类原型,减少执行一次父类构造。
这是最成熟的方法,也是现在库实现的方法。 babel 对 ES6 继承的转化也是使用了寄生组合式继承。
function Parent(name) { // 1.传参
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {
Parent.call(this, 'Jack'); // 2.执行父类构造
}
// 不能直接等于 Parent.prototype,要等于其浅拷贝。
Child.prototype = Object.create(Parent.prototype); // 3. 指向父类原型 浅拷贝
Child.prototype.constructor = Child;
4.6 ES6 的 extends 继承
Class 作为构造函数的语法糖,有prototype属性和__proto__属性,因此存在两条继承链。
(1)子类的__proto__属性表示构造函数的继承,指向父类;
(2)子类prototype属性的__proto__属性表示方法的继承,指向父类的prototype属性。
Child.__proto__ === Parent // true
Child.prototype.__proto__ === Parent.prototype // true
类的继承是按照下面的模式实现的:
class Parent {}
class Child {}
// 方法继承,子类的实例继承父类的实例
Object.setPrototypeOf(Child.prototype, Parent.prototype);
// 构造函数继承,子类继承父类的静态属性
Object.setPrototypeOf(Child, Parent);
参考文章