一、类的继承与对象的原型继承:
JavsScript不存在“类”的概念,都是“对象”。Java中如果想拥有某个类的属性和方法,需要使用extends关键字继承这个类。但是JavaScript不同于Java中的类的继承,如果想拥有像某个对象的属性和方法,需要使用prototype指定对象的原型对象。例如:
var tom = {
name: 'tom',
play: function () {
console.log(this.name + ' like playing...');
}
};
现在要创建另一个对象Jim,但是Jim除了像tom一样,有名字、喜欢玩之外,他还爱学习,最笨的办法可以这样定义:
var jim = {
name: 'jim',
play: function () {
console.log(this.name + ' like playing...');
},
study: function () {
console.log(this.name + ' like studying...');
}
};
但如果使用原型可以这样定义:
var jim = {
name: 'jim',
study: function () {
console.log(this.name + ' like studying...');
}
};
jim.__proto__ = tom;
jim.name //jim
jim.play(); //jim like playing...
jim.study(); //jim like studying...
在编写JavaScript代码时,不要轻易用__proto__去改变对象的原型,按照标准__proto__是不对外公开的,但是chrome的引擎却将他暴露了出来成为了一个公有属性,我们可以对其进行访问和赋值。但IE浏览器是不能访问这个属性的,所以不推荐大家直接操作这个属性,以免造成浏览器兼容问题。通常我们可以使用Object.create(obj),传入原型对象即可创建一个以指定对象为原型的新对象,比如:
var jim = Object.create(tom);
jim.name = 'jim';
jim.study = function(){
console.log(this.name + ' like studying...');
};
jim.name //jim
jim.play(); //jim like playing...
jim.study(); //jim like studying...
二、原型:
JavaScript对每个创建的对象都会设置一个原型__proto__,指向它的原型对象。当我们访问某个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined。prototype是函数的一个属性,只有Function类型的对象才具有prototype属性。例如创建一个数组对象:
var arr = [1, 2, 3];
其原型链为:arr ——> Array.prototype ——> Object.prototype ——> null,Array.prototype定义了indexOf()、shift()等方法,因此在所有的Array对象上都可以直接调用这些方法。再比如创建一个函数结象:
function foo() {
return 0;
}
函数也是一个对象,它的原型链是:foo ——> Function.prototype ——> Object.prototype ——> null,由于Function.prototype定义了apply()等方法,因此所有函数都可以调用apply()方法。很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花很多时间查找而变得更慢,因此要注意不要把原型链搞得太长。
三、构造函数:
可以把构造函数理解为一个普通的函数,但是这个普通的函数可以在调用时使用new关键字调用,并返回新创建的对象(不需要显式写return this),this则会自动指向这个新创建的对象。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写。另外:新创建的对象的constructor属性始终指向创建该对象的构造函数本身。例如:
function Student(name) {
this.name = name;
this.study = function () {
console.log(this.name + ' is studying ...');
}
}
var tom = new Student('tom');
tom.name //tom
tom.study() //tom is studying ...
tom.constructor //Student(name) {
// this.name = name;
// this.study = function () {
// console.log(this.name + ' is studying ...');
// }
//}
其原型链为:tom ——> Student.prototype ——> Object.prototype ——> null,用关键字new创建的对象还获得了一个constructor属性,该属性指向这个构造函数本身。关系如下图所示:
tom.constructor === Student.prototype.constructor; //true
Student.prototype.constructor === Student; //true
Object.getPrototypeOf(tom) === Student.prototype; //true
tom instanceof Student; //true
如果修改Student的prototype的某个属性,Student创建的对象的constructor依然为Student
//修改Student原型的某个属性,而不是全部覆盖其原型
Student.prototype.study = function() {
console.log(this.name + ' is not studying ...');
};
var b = new Student("Bob");
b.constructor===Student //true
但如果覆盖掉Student的prototype,Student创建的对象的constructor则变为Object
//覆盖Student的原型
Student.prototype = {
study: function(){
console.log(this.name + ' is not studying ...');
}
};
var b = new Student("Bob");
b.constructor===Student //false
b.constructor===Object //true
这是因为覆盖Student的prototype时实际做的操作是:
Student.prototype = new Object({
study: function(){
console.log(this.name + ' is not studying ...');
}
});
所以对象的constructor指向的是Object而非Student,可以用以下方法修正这个错误:
Student.prototype.constructor=Student;
四、原型链的维护:
方式1:
基于上面的示例,如果创建SeniorStudent对象,例如:
function SeniorStudent(name) {
Student.call(this, name);
this.seniorStudy = function () {
console.log(this.name + ' is seniorStudying ...');
}
}
要使原型链变为:new SeniorStudent() ——> SeniorStudent.prototype ——> Student.prototype ——> Object.prototype ——> null,因为现在的SeniorStudent.prototype和Student.prototype的原型链都是到Object.prototype的,所以需要重新维护原型链:需要借助一个中间对象来实现正确的原型链,这个中间对象的prototype要指向Student.prototype,SeniorStudent.prototype要指向这个中间对象的对象,SeniorStudent.prototype的constructor属性要指向SeniorStudent本身,关系如下图所示:
代码实现如下:
//空函数F:
function F() {
}
//把F.prototype指向Student.prototype
F.prototype = Student.prototype;
//把SeniorStudent.prototype指向中间对象F的对象
SeniorStudent.prototype = new F();
//把SeniorStudent.prototype的constructor修复为SeniorStudent
SeniorStudent.prototype.constructor = SeniorStudent;
可以验证该原型链已修改为期望的顺序:
//创建lilei对象
var lilei = new SeniorStudent('LiLei');
lilei.name; //LiLei
lilei.study(); //LiLei is studying ...
lilei.seniorStudy(); //LiLei is seniorStudying ...
//验证原型
lilei.__proto__ === SeniorStudent.prototype; //true
lilei.__proto__.__proto__ === Student.prototype; //true
//验证继承关系
lilei instanceof SeniorStudent; //true
lilei instanceof Student; //true
如果把修改原型链这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码如下:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
当然,如果不想用中间对象这么麻烦,只需要最核心的一行代码也可以实现,只是没有像原生的原型链那么完美。
SeniorStudent.prototype = new Student();
方式2:
使用新关键字class,从ES6开始正式被引入到JavaScript中。首先用关键字class创建Student类。
class Student {
constructor(name) {
this.name = name;
}
study() {
console.log(this.name + ' is studying ...');
}
}
var tom = new Student('tom');
tom.name //tom
tom.study() //tom is studying ...
如果要继承Student类创建一个新类,可以这样写:
class SeniorStudent extends Student {
constructor(name) {
super(name); //记得用super调用父类的构造方法!
}
seniorStudy () {
console.log(this.name + ' is seniorStudying ...');
}
}
ES6引入的class关键字和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码,继承的方式更接近Java类的继承方式!
参考:Douglas Crockford《JavaScript语言精粹》
http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html
https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434499763408e24c210985d34edcabbca944b4239e20000