Javascript最开始只是网景公司为了浏览器可以与网页互动设计出的一种脚本语言,因为那个时候面向对象编程极为盛行,它的设计者Brendan Eich受到了影响,Javascript里面所有的数据都是对象。但正因为是这样,必须有一种机制把对象联系起来,所以Brendan Eich最后设计了继承。
new
因为JS只是作为一个脚本语言出生的,Brendan Eich并不想让他过于正式和难以学习,所以没有引入类(class)的概念。
但是他考虑到,C++和Java都使用new命令生成实例,因此他把new命令引入Javascript,用来生成一个实例对象,并且new命令后面跟的不是类,而是构造函数。
举例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。
function DOG(name) {
this.name = name;
}
对这个构造函数使用new,就会生成一个狗对象的实例。
var dog1 = new DOG('乐乐');
alert(dog1.name); // 乐乐
但是,用构造函数生成的实例对象有一个缺点就是不能共享属性和方法。每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。
原型链继承
JavaScript使用了一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance)。
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针. 如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.
基本思想:将父类的实例作为子类的原型。
function Dog(name) {
this.name = name
this.kind = '金毛'
}
Dog.prototype.wangwang = function() {
alert('汪汪汪')
}
let dog1 = new Dog('乐乐')
dog1
//Dog {name: "乐乐", kind: "金毛"}
dog1.wangwang()
//汪汪汪
这种方法本质是通过将子类的原型指向父类的实例实现的,也就是通过让 dog1.proto__ 指向 Dog.prototype,将父类的实例作为子类的原型,原型对象的所有属性被所有实例共享。
dog2 = new Dog('大黄')
dog1.__proto__===Dog.prototype // true
dog2.__proto__===Dog.prototype // true
要注意的是,如果修改了原型对象所有实例也会收到影响。
Dog.prototype.wangwang = '我不想叫了'
dog2.wangwang
"我不想叫了"
使用了原型链后, 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined.
优点
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
-
缺点
当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
- 在创建子类型时,不能向父类型的构造函数中传递参数;
- 不能实现多继承。
借用构造函数
因为原型链中的缺点,我们开始使用一种叫做借用构造函数(constructor stealing)的技术(也叫经典继承)。
基本思想:在子类型构造函数的内部通过call调用父类型构造函数,这是继承中唯一不涉及到prototype的继承。
function Dog(name) {
this.name = name
this.kind = '金毛'
}
Dog.prototype.wangwang = function() {
alert('汪汪汪')
}
function dog(name) {
Dog.call(this,name)
}
let dog1 = new dog('乐乐')
dog1
优点
- 保证了原型链中引用类型值的独立,不再被所有实例共享
- 子类型创建时也能够向父类型传递参数
-
缺点
实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
组合继承
组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。
基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function Dog(name) {
this.name = name;
this.kind = ["white", "black"];
}
Dog.prototype.sayName = function() {
alert(this.name);
};
function dog(name, age) {
Dog.call(this, name); //继承实例属性,第一次调用Dog()
this.age = age;
}
dog.prototype = new Dog(); //继承父类方法,第二次调用Dog()
dog.prototype.sayAge = function() {
alert(this.age);
}
var dog1 = new dog("乐乐", 5);
dog1.kind.push("yellow");
console.log(dog1.kind);
dog1.sayName();
dog1.sayAge();
var dog1 = new dog("大黄", 10);
console.log(dog1.kind);
dog1.sayName();
dog1.sayAge();
优点
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
-
缺点
组合继承调用了两次父类构造函数,生成了两份实例, 造成了不必要的消耗,
class继承
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过
class
关键字,可以定义类。
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
Class 可以通过extends
关键字实现继承,子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
上面代码中,hello()
是A
类的静态方法,B
继承A
,也继承了A
的静态方法。