构造函数的prototype属性即是实例对象的原型对象。 Person.prototype === person.__proto__
函数的prototype和实例的proto都是一个对象。
constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。
1.原型链继承
1.1 实现
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () { console.log(this.name) }
function Man () {}
Man.prototype = new Person('tom')
const man1 = new Man() // 无法创建自己的属性,所有属性方法都是从prototype上拿父类的
const man2 = new Man()
这种继承方式很直接,为了获取Person的所有属性方法(实例上的和原型上的),直接将父类的实例 new Person(‘tom’) 赋给了子类的原型,其实子类的实例man1,man2本身是一个完全空的对象,所有的属性和方法都得去原型链上去找,因而找到的属性方法都是同一个。
所以直接利用原型链继承是不现实的。
1.2 核心
1.3 优缺点
优点:简单,易于实现
缺点:子类所有的属性方法都来自父类,共享属性方法,父类一改动所有的子类都变化了。
2. 利用构造函数继承
2.1 实现
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () { console.log(this.name) }
function Man (name, age) {
// 利用父类的构造函数来给自己设置属性,通过apply改变了this指向,将属性设置在当前对象中
// 但是new Man()的时候, 做的是 1.生成一个新对象 2.将新对象的__proto__ = Man.prototype 3.将this指向当前新对象 4.返回新对象(默认情况,也可以返回别的东西)
// Person原型上的方法,就完全没有被使用,也无法被Man的实例所使用
Person.apply(this, [name, age])
}
const man1 = new Man('jim')
const man2 = new Man('lily')
man1.name === man2.name // false
man1.say() // say is not a function
这里子类的在构造函数里利用了apply去调用父类的构造函数,从而达到继承父类属性的效果,比直接利用原型链要好的多,至少每个实例都有自己那一份资源,但是这种办法只能继承父类的实例属性,因而找不到say方法,为了继承父类所有的属性和方法,则就要修改原型链,从而引入了组合继承方式。
2.2 核心
借父类的构造函数来增强子类实例,等于是把父类的实例属性复制了一份给子类实例装上了(完全没有用到原型)
2.3 优缺点
优点:
- 解决了子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类构造函数传参
缺点:
- 无法实现函数复用
3. 组合继承(最常用)
目前我们的借用构造函数方式还是有问题(无法实现函数复用),没关系,接着修复,于是有了组合继承
3.1 具体实现
function Person(name, age) {
// 只在此处声明属性
this.name = name
this.age = age
}
// 在此处声明函数
Person.prototype.say = function () { console.log(this.name) }
//Super.prototype.fun3...
function Man(...args){
Person.apply(this, args); // 核心
// ...
}
// 核心 把父类的实例作为子类的原型。
// 那么子类实例,可以从 子类实例.__proto__ 访问到父类的实例(即new Person()) 继而父类实例.__proto__ 到达 构造函数Person的prototype,say方法拿到
Man.prototype = new Person();
var man1 = new Man('tom', 18); // {name: "tom", age: 18}
var man2 = new Man('lily',20); // {name: "lily", age: 20}
alert(man1.say === man2.say); // true
3.2 核心
把实例函数都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过 Person.apply(this)
继承父类的基本属性和引用属性并保留能传参的优点;
通过 Man.prototype = new Person()
继承父类函数,实现函数复用
3.3 优缺点
优点:
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
(一点小瑕疵)子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的。。。又是内存浪费,比刚才情况好点,不过确实是瑕疵
4.寄生组合继承(最佳方式)
4.1 实现
function createPure(prototype){ // 创造一个纯净的对象
var F = function(){}
F.prototype = prototype // prototype中有constructor属性,记录这构造函数是Person
// F.prototype.constructor = F // 这里不要修改constructor,修改了constructor,继承就断了,即Man继承于F,看不到是继承自Person了
return new F()
}
function Person(name){
// 只在此处声明属性
this.val = 1
this.name = name
}
// 在此处声明函数
Person.prototype.say = function(){ console.log(this.name, this.val) }
function Man(...args){
Person.apply(this, args); // 核心
// ...
}
Man.prototype = createPure(Person.prototype) // 跟组合继承相比,createPure切割掉了Person实例上的属性
// 如果不设置constructor,Man的实例查找construtor,会找man.__proto__ (即Man.prototype),而这个对象是createPure创建的空对象,也没有constructor属性
// 继而从空对象.__proto__查找(即 F.prototype),找到了 constructor: Person, 而不是Man
// 所以修改了函数的prototype,就要注意同时修改prototype的constructor
Man.prototype.constructor = Man
var man = new Man('tom');
console.log(man.val); // 1
console.log(man.name); // tom
createPure()
函数返回 原型为prototype的没有实例属性的对象;Man.prototype = createPure(Person.prototype)
建立了原型链,继承父类的原型属性,用createPure()
函数的作用是作为子类原型的父类实例没有实例属性。即切除掉了多余的实例属性。Man.prototype.constructor = Man
改变原型会改变子类持有的构造属性,改变prototype属性后应该修改 constructor
4.2 核心
用createPure(Person.prototype)
切掉了原型对象上多余的那份父类实例属性
**
4.3 优缺点
优点:完美了
缺点:理论上没有了(如果用起来麻烦不算缺点的话。。)
P.S.用起来麻烦是一方面,另一方面是因为寄生组合式继承出现的比较晚,是21世纪初的东西,大家等不起这么久,所以组合继承是最常用的,而这个理论上完美的方案却只是课本上的最佳方式了
5.补充
其实介绍完上面的完美方案就可以结束了,但从组合继承到完美方案好像有一段不小的思维跳跃,有必要把故事说清楚
5.1 原型式
5.1.1 实现
function createPure(obj){ // 生孩子函数
var F = function(){};
F.prototype = obj;
return new F();
}
function Person(name){
this.val = 1;
this.name = name;
}
// 生孩子
var man = createPure(new Person('tom')); // 核心 生成的对象拥有父类的属性和方法
// 增强 增加自己的属性
man.attr1 = 1;
man.attr2 = 2;
5.1.2 核心
用生孩子函数得到得到一个“纯洁”的新对象(“纯洁”是因为没有实例属性),再逐步增强之(填充实例属性)
ES5提供了Object.create()函数,内部就是原型式继承,IE9+支持
**
5.1.3 优缺点
优点:从已有对象衍生新对象,不需要创建自定义类型(更像是对象复制,而不是继承。。)
缺点:
原型引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
无法实现代码复用(新对象是现取的,属性是现添的,都没用函数封装,怎么复用)
P.S.这东西和继承有很大关系吗?为什么尼古拉斯把它也列为实现继承的一种方式?关系不大,但有一定关系
5.2 寄生式
5.2.1 具体实现
function createPure(obj){ // 生孩子函数 beget:龙beget龙,凤beget凤。
var F = function(){};
F.prototype = obj;
return new F();
}
function Person(name){
this.val = 1;
this.name = name;
}
function getSubObject(obj){
// 创建新对象
var clone = createPure(obj); // 核心
// 增强
clone.attr1 = 1;
clone.attr2 = 2;
return clone;
}
var sub = getSubObject(new Super());
5.2.2 核心
给原型式继承穿了个马甲而已,看起来更像继承了(上面介绍的原型式继承更像是对象复制)
注意:createPure函数并不是必须的,换言之, 创建新对象 -> 增强 -> 返回该对象
,这样的过程叫寄生式继承,新对象是如何创建的并不重要(用createPure生的,new出来的,字面量现做的。。都可以)
5.2.3 优缺点
优点:还是不需要创建自定义类型
缺点:
无法实现函数复用(没用到原型,当然不行)
6. class extends
ES6的class是一个语法糖(不完全是语法糖,super()没有完全对等的es5表示)
// class相当于es5中构造函数
// class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
// class中定义的所有方法是不可枚举的
// class中只能定义方法,不能定义对象,变量等
// class和方法内默认都是严格模式
// es5中constructor为隐式属性
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
say (sth) { console.log(this.name, sth)}
}
constructor()
就是构造方法,constructor()
默认返回实例对象(即this),也可以指定返回定外一个对象。 this
关键字代表实例对象。
ES6的类,完全可以看做是构造函数的另一种写法,类的数据类型就是函数,类本身就指向构造函数。 Person.prototype.constructor === Person
构造函数的 prototype
在 类
上也继续存在,事实上, 类的所有方法都是定义在 类的prototype上
。
ES6规定,class内部只有方法/静态方法,没有静态属性。与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上。
类不存在变量提升, 必须先声明类,才可以使用。
es5 继承 vs es6 继承
通过前面es5 寄生组合继承方式,我们可以知道,es5的继承,是 Person.apply(this, args)
。 也可以理解成,子类的构造函数,将this传给了父类构造函数,利用父类构造函数,将属性添加到子类this上。
// ES5
function Man (...args) {
Person.apply(this, args)
}
// new Man() 的时候,创建了自己的this, 借用父类函数,给自己的this添加属性。然后将Man.prototype (即利用Person.prototype产生的对象)绑定在实例的___proto__上
// 即先创建自己的this,然后借用父类给this添加方法
es6继承是先调用
super()
,super()
返回了一个对象,有着父类的属性和方法,super()返回的值作为自己的this,然后用子类的构造函数修改this ——我的理解super()
的作用是由父类构造器把this返回给子类构造器。——贺师俊 具体的实现细节不是很清楚,但是super()是对父类属性方法的继承。
// ES6
class Person {
constructor (name) {
this.name = name
}
say() { console.log('name:', this.name) }
}
class Man extends Person {
constructor (name, age) {
super(name) // 调用父类的constructor,父类的constructor返回的是一个实例,这个实例被当做子类的this
this.age = age // 添加自己的实例属性
}
say () { console.log('age:', this.age)} // 屏蔽父类的同名方法
}