概念
- 在 JavaScript 中,函数是允许拥有属性的。所有的函数会有一个特别的属性
prototype
,叫做函数的原型对象;同时原型对象中,有一个叫做constructor的属性,指向这个函数本身。 - 基于构造函数创建对象 (new) 后,每个实例对象都有一个私有的
__proto__
属性,指向其构造函数的原型对象, es6 中利用Object.getPrototypeOf()
来访问对象的原型。 - 原型对象又有自己的原型对象
__proto__
,层层向上直到某个原型对象为 null。 - 几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例,Object 的原型对象是 null。
注意:如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用 constructor 指回原来的构造函数
原型链
原型链是由__proto__串联起来的链状结构。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型 __proto__
,以及该对象的原型的原型 __proto__.__proto__
,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
注意:如何区分一个属性到底是基本的还是从原型中找到的呢?答案:hasOwnProperty
for (let item in obj) { // 会列举出obj及其原型链上的所有可枚举属性
if (obj.hasOwnProperty(item)) { ... } // hasOwnProperty是Object.prototype中的方法
}
复制代码
创建对象构造原型链
使用语法结构创建的对象
var o = {a: 1};
// 原型链: o ---> Object.prototype ---> null
// 继承 Object.prototype 的属性:hasOwnProperty 等
var a = [1];
// 原型链: a ---> Array.prototype ---> Object.prototype ---> null
// 继承 Array.prototype 的属性:forEach,indexof 等
function f(){ }
// 原型链: f ---> Function.prototype ---> Object.prototype ---> null
// 继承 Function.prototype 的属性:call,bind 等
复制代码
使用构造函数创建的对象
在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。
function Foo() {
this.value = 1;
}
Foo.prototype.showValue = function(){ // 公共方法定义在构造函数的原型上
console.log(this.value);
};
var foo = new Foo();// 原型链: foo ---> Foo.prototype ---> Object.prototype ---> null
复制代码
注意:我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
使用 Object.create 创建的对象
原型作为 Object.create 参数,生成一个新对象。
var fooProto = {
showValue: function(){ console.log(this.value); }
};
var foo = Object.create(fooProto, {
value: {
value: 1,
writable: true,
enumerable: true,
configurable: true
}
}); // 原型链:foo ---> fooProto ---> Object.prototype ---> null
复制代码
使用 class 关键字创建的对象
ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。
这些新的关键字包括 class, constructor,static,extends 和 super。
class Foo {
constructor(value) {
this.value = value;
}
showValue(){ console.log(this.value); }
}
var foo = new Foo(1);
复制代码
继承
我们知道面对对象(OOP)的三大特点是:继承,封装,多态(重载,重写)
js并不是严格的面对对象的语言,因为js的面对对象也是基于原型链实现的。
首先定义需要继承的父类:
function Parent(name, height) {
this.name = name;
this.height = height;
this.getName = function(){
console.log('Name:', this.name);
};
}
Parent.prototype.getHeight = function(){
console.log('Height:', this.height);
};
复制代码
利用构造函数实现继承
关键: 在函数对象内调用父类的构造函数,使得自身获得父类的方法和属性。
function Child(name, height, age) {
Parent.apply(this, [name, height]); // 改变 Parent 的 this 指向并运行,使得 Child 实例拥有了 Parent 的属性和方法
this.age = age;
}
let ch = new Child('a', 170, 21); // 原型链:ch ---> Child.prototype ---> Object.prototype ---> null
复制代码
优点:
- 子类实例属性不共享
- 创建的子类实例可以向父类传递参数
- 可以实现多继承,用 call / apply 改变父类的this
缺点:
- 实例是子类的实例,不是父类的
ch instanceof Parent === false
- 只能继承父类的实例属性和方法,不能继承父类原型上的方法(
ch.getHeight()
会报错) - 无法实现函数复用,每个子类都有父类函数的属性和方法的副本,占用很大的内存,而且当父类的方法发生改变了时,已经创建好的子类实例并不能更新方法
利用原型链实现继承
function Child(age) {
this.age = age;
}
let pa = new Parent('a', 170);
Child.prototype = pa; // 关键:Child 的原型设为 Parent 的实例
Child.prototype.constructor = Child; //为了不破坏原型链,将constructor指向本身
Parent.prototype.show = function(){
console.log('show');
} // 父类新加的方法,所有子类实例都可以访问
let ch = new Child(21); // 原型链:ch ---> pa ---> Parent.prototype ---> Object.prototype ---> null
复制代码
优点:
- 子类与父类的关系为指向关系,实例是子类的实例,也是父类的实例
- 父类新增的原型方法或属性,子类都能访问
缺点:
- 原型属性上的引用类型值会被所有实例共享,实例之间相互影响
- 给子类型原型添加属性和方法必须在替换原型之后】
- 创建子类型实例时无法向父类型的构造函数传参
组合模式
结合构造继承和原型继承的各自优点来进行对父类的继承。用构造继承属性,而原型继承方法。
function Child(name, height, age) {
Parent.apply(this, [name, height]);
this.age = age;
}
Child.prototype = new Parent('a', 170);
let ch = new Child('a', 170, 21);
复制代码
优点:
- 子类可向父类传参
- 实例既是子类的实例,也是父类的实例
- 多个实例之间不存在公用父类的引用属性的问题
- 实例可以继承父类实例的属性和方法,也可以继承原型上的属性和方法
缺点:
- 两次调用父类的构造函数,生成了两份实例,相同的属性既存在于实例中也存在于原型中
寄生组合继承
不必为了指定子类的原型而调用父类的构造函数,直接拿到父类的原型对象并继承。
与组合继承区别在于没有实例化父类对象。
几乎结合了构造、原型链、组合继承的所有优点。
function Child(name, height, age) {
Parent.apply(this, [name, height]);
this.age = age;
}
(function(){
let Temp=function(){}; // 创建一个临时的类
Temp.prototype=Parent.prototype; //子类的原型指向父类的实例
Child.prototype=new Temp();
})()
let ch = new Child('a', 170, 21);
复制代码
es6 类的继承语法
语法更直观,书写更简便。
class Parent {
constructor (name, height){
this.name = name;
this.height = height;
}
static show() {
console.log('show');
}
getName() {
return this.name;
};
getHeight() {
return this.height;
};
}
class Child extends Parent {
constructor (name, height, age) {
super(name, height);
this.age = age;
}
}
let ch = new Child('a', 170, 21);
复制代码
注意:当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。