继承及继承的方式
继承
JS中继承的概念:
- 通过【某种方式】让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承【并不是所谓的xxx extends yyy】
为什么要使用继承?
- 有些对象会有方法(动作、行为),而这些方法都是函数,如果把这些方法和函数都放在构造函数中声明就会导致内存的浪费
- 把子类中共同的成员提取到父类中,实现代码重用。
function Person(){
this.say = function(){
console.log("你好")
}
}
var p1 = new Person();
var p2 = new Person();
var p3 = new Person();
var p4 = new Person();
...
console.log(p1.say === p2.say); //false 由于say方法可能功能相似,但是不是同一个方法(没有指向同一块内存,会造成内存浪费)
继承的方式
实现继承首先需要一个父类,在js中实际上是没有类的概念,在es6中class虽然很像类,但实际上只是es5上语法糖而已。
// 父类
function Person(name, age, sex){
this.name = name;
this.age = age;
this.sex = sex;
}
// 子类
function Student(name, age, sex){
this.name = name;
this.age = age;
this.sex = sex;
}
第一种方式:原型链继承(父类的实例作为子类的原型)
function Student(){}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.name = 'zhangsan';
Student.prototype.age= 18;
var s1 = new Student();
- 优点: 简单易于实现,父类新增的原型方法/原型属性,子类都能访问
- 缺点:
1. 要想为子类新增属性和方法,必须要在 new 父类构造函数的后面执行,不能放到构造器中
2. 无法实现多继承
3. 创建子类实例时,不能向父类构造函数中传参数
第二种方式:借用构造函数实现继承 .call() .apply()
function Student(name,age, sex, score){
this.score = score;
Person.call(this, name, age, sex);
// ->等价于
//Person.apply(this, [name, age, sex])
}
var s1 = new Student('zhangsan', 18, 'male', 90);
- 场景:适用于2种构造函数之间逻辑有相似的情况,如 Student(), Person()
- 优点:
1. 解决了子类构造函数向父类构造函数中传递参数
2. 可以实现多继承(call或者apply多个父类) - 缺点:
1. 方法都在构造函数中定义,无法复用。
2. 不能继承原型属性/方法,只能继承父类的实例属性和方法。
第三种方式:组合式继承(借用构造函数 + 原型链继承)
- 核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Student(name,age, sex, score){
this.score = score;
Person.call(this, name, age, sex);
// ->等价于
//Person.apply(this, [name, age, sex])
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.exam = function(){
console.log('考试了')
}
var s1 = new Student('zhangsan', 18, 'male', 90);
- 优点:
1. 可以继承属性和方法,并且可以继承原型的属性和方法
2. 函数可以复用
3. 不存在引用属性问题 - 缺点:
1. 调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
第四种方式:拷贝继承(混入继承)
- 场景:有时候想使用某个对象中的属性,但是又不能直接修改它,于是就可以创建一个该对象的拷贝
- 实现1:
var source={name:"李白",age:15}
var target={};
target.name=source.name
target.age=source.age;
- 上面的方式很明显无法重用,实际代码编写过程中,很多时候都会使用拷贝继承的方式,所以为了重用,可以编写一个函数把他们封装起来:
function extend(target,source){
for(key in source){
target[key]=source[key];
}
return target;
}
extend(target,source)
-
由于拷贝继承在实际开发中使用场景非常多,所以很多库都对此有了实现
- jquery:$.extend
-
es6中有了对象扩展运算符仿佛就是专门为了拷贝继承而生:
var source={name:"李白",age:15}
var target={ ...source }
var target={ ...source, age:18}
第六种方式:原型式继承
- 场景:
- 创建一个纯洁的对象
- 创建一个继承自某个父对象的子对象
- 使用方式:
- 空对象:Object.create(null)
var o1={ say:function(){} } var o2=Object.create(o1);
其它方式:寄生继承、寄生组合继承
bind()、call()、apply() 区别:
- 共同点:改变this指向。
- 不同点:
示例:function fn(x,y){ console.dir(this); //this --> window console.log(x + y); } fn(2, 3)
- bind() 改变
this
的指向,不会调用函数,返回一个新的函数;var o = {name:'wang'}; var fn1 = fn.bind(o, 2, 3); fn1(); // this --> o
- call() 方法调用一个函数, 其具有一个指定的
this
值和分别地提供的参数(参数的列表)。var o = {name:'wang'}; fn.call(o, 2, 3); // this --> o
- apply() 方法调用一个函数, 其具有一个指定的
this
值,以及作为一个数组(或类似数组的对象)提供的参数。var o = {name:'wang'}; fn.apply(o, [2, 3]); // this --> o
函数内 this
指向的不同场景
函数的调用方式决定了 this
指向的不同:
调用方式 | 非严格模式 | 备注 |
---|---|---|
普通函数调用 | window | 严格模式下是 undefined |
构造函数调用 | 实例对象 | 原型方法中 this 也是实例对象 |
对象方法调用 | 该方法所属对象 | 紧挨着的对象 |
事件绑定方法 | 绑定事件对象 | |
定时器函数 | window |