[TOC]
对象原型继承
面向对象编程(Object Oriented Programming,OOP)是目前主流编程范式。
它将真实世界各种复杂的关系抽象为一个个对象,然后由对象之间分工与合作,完成对真实世界的模拟。
面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
- 对象是单个实物的抽象
- 对象是一个容器,封装了属性(property)和方法(method)
典型的面向对象语言(C++、Java)都有类(class)的概念:类就是对象的模板,对象是类的实例。
而JavaScript语言的对象体系是基于构造函数(constructor)和原型链(prototype).
构造函数相当于其他语言的类
原型链类相当其他语言的继承
1. 构造函数:JS的类
JavaScript语言中构造函数(constructor):
- 是对象的模板,描述实例对象的基本结构。实例对象的属性和方法,可以定义在构造函数内部
- 是用来生成实例对象的函数
- 是一个普通函数,但有自己的特征和用法
- 为了与普通函数区别,构造函数名字第一个字母通常大写
构造函数的特点:
- 函数体内部使用了this关键字,代表了所要生成的对象实例。
- 生成对象的时候必须使用new命令。
- 如果不用new命令就执行则会成为普通函数,此时会给this指向的对象创建变量,可使用严格模式防止忘记new命令,或在内部判断是否使用了new命令
function Foo(foo, bar){
// 'use strict'; // 在严格模式下,如果不指定this,则this是undefined
// 或者使用判断是否使用了 new命令
// if (!(this instanceof Foo)) {return new Foo(foo) }
this.tt = 200;
}
// 使用上面注释部分代码可解决不使用new问题
Foo() // 这里报错 TypeError: Cannot set property 'tt' of undefined
new Foo()).tt // 正常访问
1.1 使用new命令生成一个对象
对构造函数使用new命令后:
- 创建一个空对象(obj),作为将要返回的对象的实例
- 将obj的原型指向构造函数的prototype属性
- 将obj赋值给函数内部的this关键字
- 开始执行构造函数的内部代码
也就是说构造函数就是操作一个空对象将其“构造”为需要的样子。
使用new命令注意:
- new命令总是返回一个对象要么是实例对象,要么是return语句指定的对象,如果return的不是对象,则忽略return
- 如果普通函数(内部没有this),new命令则会返回空对象
- new命令本身就可以执行构造函数,所以构造函数可以带括号,也可以不带。
function A() { this.one = 200; return 30;}
console.log((new A()) === 1000) // false
function B() { this.one = 200; return {tt: 10};}
console.log((new B()).tt) // 10 构造函数使用括号
console.log((new B.tt) // 10 构造函数没有使用括号
function hello(){return 'hello';}
console.log((new hello()))
1.1.1 new.target
函数内部有一个属性:new.target,如果使用new命令调用函数则new.target指向当前函数,否则为undefined。
可使用该属性来判断调用函数的手是否使用了了new命令
function f() { console.log(new.target === f); }
f() // false
new f() // true
1.2 Object.create() 创建实例对象
Object.create
:是ES5方法,以现有的对象为模板生成新的实例对象。
创建一个对象:如果没有构造函数,只有对象,这个时候 Object.create就用到了,或者自己写一个克隆函数的方法。
var person1 = { name: 'jack', };
var person2 = Object.create(person1);
console.log(person2.name)
2. 对象原型的概述
2.1 构造函数的缺点
JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。
通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () { console.log('喵喵'); };
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow // false
cat1和cat2是同一个构造函数的两个实例,由于meow
方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow
方法。这既没有必要,又浪费系统资源,因为所有meow
方法都是同样的行为,完全应该共享。
这个问题的解决方法,就是JS的原型对象(prototype)
2.2 函数的prototype属性的作用
JS的继承机制:原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
上面代码中,构造函数Animal
的prototype
属性,就是实例对象cat1
和cat2
的原型对象。原型对象上添加一个color
属性,结果,实例对象都共享了该属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
上面代码中,实例对象cat1
的color
属性改为black
,就使得它不再去原型对象读取color
属性,后者的值依然为yellow
。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
2.3 原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype),任何一个对象,都可以充当其他对象的原型。
由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
实例对象的原型一层层上溯,最终都可以上溯到Object.prototype
,即Object
构造函数的prototype
属性。这就是所有对象都象都有valueOf
和toString
方法的原因。
那么,Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头是null。
Object.getPrototypeOf(Object.prototype)
// null
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
2.4 constructor属性
prototype对象上有一个constructor属性,默认指向prototype对象所在的构造函数。可以被所有实例对象继承。
function P() {}
var p = new P();
P.prototype.constructor === P // true
// 对象p自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性
p.constructor === P // true
p.hasOwnProperty('constructor') // false
constructor属性作用:
可以知道实例对象是哪一个构造函数创建的。
可以通过实例对象的constructor属性新建另一个实例对象。
// 判断对象的构造函数
function F() {};
var f = new F();
f.constructor === F // true
f.constructor === RegExp // false
// 从一个实例对象新建另一个实例
function Constr() {}
var x = new Constr();
var y = new x.constructor();
y instanceof Constr // true
constructor表达了原型对象和构造函数的关联关系,如果修改了原型对象,要同时修改constructor属性,防止引用时出错。
function Person(name) { this.name = name; }
Person.prototype.constructor === Person // true
Person.prototype = { method: function () {} };
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
上面代码中,构造函数Person
的原型对象改掉了,但是没有修改constructor
属性,导致这个属性不再指向Person
。由于Person
的新原型是一个普通对象,而普通对象的constructor
属性指向Object
构造函数,导致Person.prototype.constructor
变成了Object
。
// 坏的写法
C.prototype = {
method1: function () { ... },
// ...
};
// 好的写法
C.prototype = {
constructor: C,
method1: function () { ... },
// ...
};
// 更好的写法
C.prototype.method1 = function () { ... };
如果不能确定constructor
属性是什么函数,还有一个办法:通过name
属性,从实例得到构造函数的名称。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
3. JS实现继承
3.1 原型链继承
简单来说就是将父类的实例作为子类的原型,父类新增属性和方法,子类都能访问到
function A() {
this.num = 0
this.arr = [1,2,3,4]
}
function B() {
}
B.prototype = new A()
var b1 = new B()
var b2 = new B()
b1.num = 1
b1.arr.push(5)
console.log(b1.num)//1
console.log(b2.num)//0
console.log(b1.arr)//[1,2,3,4,5]
console.log(b2.arr)//[1,2,3,4,5]
上述代码以A的实例作为B的原型,B的实例b1和b2均继承了A的num属性和arr属性,达到了继承的目的。但是当我们修改b1的num值时,b2的值不受影响,这确实符合我们的预期,但是当我们修改b1的arr时,b2的arr也改变了,这显然不是我们想看到的,这就是原型继承的缺陷
综合评价:可以继承原型属性,实现简单,但是创建子类实例时无法向父类构造函数传参,并且由于原型对象是共享的,所以如果某个子类实例改变了原型属性,那么所有实例的该属性都会改变,注意是对象属性。
3.2 构造函数继承
第一步、是在子类的构造函数中,调用父类的构造函数。目的是让子类实例继承父类实例的属性和方法。
第二步、是让子类的原型指向父类的原型,目的是让子类继承父类原型上的属性和方法。
注意:不能让子类的原型直接等于父类原型,如果这样做那么子类原型和父类原型是同一个对象,那么子类在原型上添加方法,父类原型也修改了,导致父类对象也能获取到这个方法。
function Super() { this.myName = 'super' }
// 第一步:在实例上调用父类的构造函数Super,让子类实例继承父类实例的属性和方法。。
function Sub(value) {
Super.call(this);
this.prop = value;
}
// 第二步:设置子类的原型
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
var sub = new Sub();
sub instanceof Super // true
sub instanceof Sub // true
另外一种实现,子类构造函数的prototype等于父类的实例:
// 这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。
Sub.prototype = new Super();
上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
}
4. 多重继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
function M1() { this.hello = 'hello'; }
function M2() { this.world = 'world'; }
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
上面代码中,子类S
同时继承了父类M1
和M2
。这种模式又称为 Mixin(混入)。
5. instanceof运算符:判断对象的类型
instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
instanceof运算符的左边是实例对象,右边是构造函数,它会检查构造函数的原型对象是否在对象的原型链上,因此同一个实例对象,可能会对多个构造函数都返回true
。
有一种特殊情况,就是左边对象的原型链上,只有null
对象。这时,instanceof
判断会失真。
但是,只要一个对象的原型不是null
,instanceof
运算符的判断就不会失真。
注:
instanceof只能用于对象,不适用原始类型的值,原始值返回false。
对于
undefined
和null
,instanceOf
运算符总是返回false
。利用instanceof运算符解决调用构造函数时,忘记加new命令的问题。
var d = new Date();
d instanceof Date // true
d instanceof Object // true
Object.create(null) instanceof Object // false
'hello' instanceof String // false
undefined instanceof Object // false
function F (foo) {
if (!(this instanceof F) { } // 没有使用new操作符
}