程序员好技术文档HTML5开发JS的创建与继承,JavaScript会为每一个创建的对象设置一个原型,指向其原型对象。 当我们使用obj.xxx访问一个对象的属性时,JavaScript引擎首先在当前对象上寻找属性,如果没有找到,就去它的原型对象,如果找不到,就去所有的回到对象。prototype对象,最后如果没有找到,只能返回undefined。 例如创建一个Array对象: vararr=[1,2,3]; 其原型链为: arr---->Array.prototype---->Object.prototype---->null Array.prototype定义了indexOf()、shift()等方法,因此您可以直接在所有Array对象上调用这些方法。 当我们创建一个函数时: functionfoo(){ return0; } 函数也是一个对象,它的原型链是: foo---->Function.prototype---->Object.prototype---->null 由于Function.prototype定义了apply()等方法,所有的函数都可以调用apply()方法。 很容易认为,如果原型链很长,那么访问一个对象的属性会比较慢,因为查找需要更多的时间,所以要注意原型链不要太长。 构造函数 除了直接使用{...}来创建对象之外,JavaScript还可以使用构造函数方法来创建对象。它的用法是先定义一个构造函数: functionStudent(name){ this.name=name; this.hello=function(){ alert('Hello,'+this.name+'!'); } } 你会问,咦,这不是一个普通的函数吗? 这确实是一个普通的函数,但是在JavaScript中,你可以使用关键字new来调用这个函数并返回一个对象: varxiaoming=newStudent('小明'); xiaoming.name;//'小明' xiaoming.hello();//你好,小明! 注意,如果不写new,这是一个返回undefined的普通函数。但是,如果你写new,它就变成了一个构造函数,它绑定的this指向新创建的对象,默认返回this,也就是说最后不用写returnthis; 新创建小明的原型链为: xiaoming---->Student.prototype---->Object.prototype---->null 即小明的原型指向函数Student的原型。如果再创建xiaohong和xiaojun,这些对象的原型和xiaoming一样: xiaoming↘ xiaohong-→Student.prototype---->Object.prototype---->null xiaojun↗ 用newStudent()创建的对象也从原型中获取了一个constructor属性,指向函数Student本身: xiaoming.constructor===Student.prototype.constructor;//true Student.prototype.constructor===Student;//true Object.getPrototypeOf(xiaoming)===Student.prototype;//true xiaominginstanceofStudent;//true ?用一张图来表示这些乱七八糟的关系: 红色箭头是原型链。注意Student.prototype指向的对象是小明和小红的原型对象。这个原型对象还有一个属性构造函数,它指向Student函数本身。 另外,函数Student恰好有一个属性prototype指向xiaoming和xiaohong的原型对象,但是像xiaoming和xiaohong这样的对象没有prototype属性,不过可以使用__proto__的非标准用法来检查。 现在我们认为像xiaoming和xiaohong这样的对象“继承”自Student。 不过还有一个小问题,注意观察: xiaoming.name;//'小明' xiaohong.name;//'小红' xiaoming.hello;//函数:Student.hello() xiaohong.hello;//函数:Student.hello() xiaoming.hello===xiaohong.hello;//false xiaoming和xiaohong名字不一样,是对的,不然我们就分不清谁是谁了。 小明和小红的hello是一个函数,但是是两个不同的函数,虽然函数名和代码是一样的! 如果我们通过newStudent()创建了很多对象,那么对象的hello函数其实只需要共享同一个函数,这样可以节省很多内存。 让创建的对象共享一个hello函数,根据对象属性查找的原则,我们只需要将hello函数移动到小明和小红的共同原型,即Student.prototype中即可: 修改代码如下: functionStudent(name){ this.name=name; } Student.prototype.hello=function(){ alert('Hello,'+this.name+'!'); }; 使用new创建基于原型的JavaScript对象就是这么简单! 忘记写new怎么办 如果一个函数定义为Constructor,但是调用的时候忘记写new怎么办? 在严格模式下,this.name=name会报错,因为this绑定了undefined。在非严格模式下,this.name=name不会报错,因为this绑定了window,所以无意中创建了全局变量name。并返回undefined,这更糟糕。 所以调用构造函数的时候不要忘记写new。为了区分普通函数和构造函数,按照约定,构造函数的首字母要大写,而普通函数的首字母要小写,这样一些语法检查工具比如jslint会帮你检测缺少新的。 最后,我们还可以写一个createStudent()函数,在内部封装所有新的操作。一个常见的编程模式如下所示: functionStudent(props){ this.name=props.name||'匿名的';//默认值为'anonymous' this.grade=props.等级||1;//默认值为1 } Student.prototype.hello=function(){ alert('Hello,'+this.name+'!'); }; functioncreateStudent(props){ returnnewStudent(props||{}) } 这个createStudent()函数有几个很大的优点:首先,它不需要newtocall,二是参数很灵活,可以不传,也可以这样传: varxiaoming=createStudent({ name:'Xiaoming' }); xiaoming.grade;//1 如果创建的对象有很多属性,我们只需要传递一些需要的属性,其余的属性可以使用默认值。由于参数是一个对象,我们不需要记住参数的顺序。如果刚好从JSON中获取对象,可以直接创建xiaoming。 继承 在Java、C++等传统的Class-based语言中,继承的本质是扩展一个已有的Class,生成一个新的Subclass。 由于这类语言严格区分类和实例,所以继承实际上是类型的扩展。但是,由于JavaScript使用原型继承,我们不能直接扩展一个Class,因为没有Class这样的类型。 不过还是有办法的。我们先回顾一下Student的构造函数: functionStudent(props){ this.name=props.name||'未命名'; } Student.prototype.hello=function(){ alert('你好,'+this.name+'!'); } 和Student的原型链: 现在我们要基于Student扩展PrimaryStudent,可以先定义PrimaryStudent: functionPrimaryStudent(props){ //调用学生构造函数并绑定此变量: Student.call(this,props); this.grade=props.grade||1; } 不过,调用Student构造函数并不代表继承Student。PrimaryStudent创建的对象原型为: newPrimaryStudent()---->PrimaryStudent.prototype---->Object。prototype---->null 必须想办法修改原型链为: newPrimaryStudent()---->PrimaryStudent.prototype---->Student.prototype---->Object.prototype---->null 这样原型链就对了,继承关系就对了。基于PrimaryStudent创建的新对象既可以调用PrimaryStudent.prototype定义的方法,也可以调用Student.prototype定义的方法。 如果你想用最简单粗暴的方式做到这一点: PrimaryStudent.prototype=Student.prototype; 决不!如果是这样,PrimaryStudent和Student共享一个原型对象,那么为什么要定义PrimaryStudent? 我们必须使用一个中间对象来实现正确的原型链。这个中间对象的原型必须指向Student.prototype。为了实现这一点,参考道爷(发明JSON的Douglas)的代码,中间对象可以通过一个空函数F来实现: //PrimaryStudent构造函数: functionPrimaryStudent(props){ Student.call(this,道具); this.grade=props.grade||1; } //空函数F: functionF(){ } //将F的原型指向Student.prototype: F.prototype=Student。原型; //将PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype: PrimaryStudent.prototype=newF(); //固定PrimaryStudent原型的构造函数为PrimaryStudent: PrimaryStudent.prototype.constructor=PrimaryStudent; //继续在PrimaryStudent原型(即newF()对象)定义方法: PrimaryStudent.prototype.getGrade=function(){ returnthis.grade; }; //创建小明: varxiaoming=newPrimaryStudent({ name:'小明', 成绩:2 }); xiaoming.name;//'小明' xiaoming.grade;//2 //验证原型: xiaoming.__proto__===PrimaryStudent.prototype;//真 xiaoming.__proto__.__proto__===Student.prototype;//true //验证继承关系: xiaominginstanceofPrimaryStudent;//true xiaominginstanceofStudent;//true 用一个Figure表示新的原型链: 注意函数F只是用来桥接的,我们只新建一个F()实例,原来Student定义的原型链有没有被改变。 如果用inherits()函数封装继承的动作,还可以隐藏F的定义,简化代码: functioninherits(Child,Parent){ varF=function(){}; F.prototype=Parent.prototype; Child.prototype=newF(); Child.prototype.constructor=Child; } 这个inherits()函数可以被重用: functionStudent(props){ this.name=props.name||'未命名'; } Student.prototype.hello=function(){ alert('Hello,'+this.name+'!'); } functionPrimaryStudent(props){ Student.call(this,props); this.grade=props.grade||1; } //实现原型继承链: inherits(PrimaryStudent,Student); //为PrimaryStudent原型绑定其他方法: PrimaryStudent.prototype.getGrade=function(){ returnthis.grade; }; Summary JavaScript的原型继承实现方法是: 定义一个新的构造函数,并在内部调用call()希望“继承”这个构造函数,并绑定this; 原型链继承是借助中间函数F实现的,最好是通过封装的inherits函数; 继续在新构造函数的原型上定义新的方法。 ES6类继承 新关键字class从ES6开始正式引入JavaScript。类的目的是使定义类更容易。 先回顾一下用函数实现Student的方法: functionStudent(name){ this.name=name; } Student.prototype.hello=function(){ alert('你好,'+this.name+'!'); } 如果用新的class关键字写Student,可以这样写: classStudent{ constructor(name){ this.name=name; } hello(){ alert('Hello,'+this.name+'!'); } } 对比一下可以发现class的定义包括原型对象上定义的构造函数和函数hello()(注意这里没有function关键字),从而避免了Student.prototype.hello=function(){...}这样零散的代码。 最后创建Student对象的代码和上一章一模一样: varxiaoming=newStudent('Xiaoming'); xiaoming.hello(); classinheritanceAnother对象的巨大好处是继承更方便。想一想我们需要编写多少代码才能从Student派生PrimaryStudent。现在,原型继承的中间对象,原型对象的构造函数等都不需要考虑了,直接通过extends实现即可:{ super(名字);//记得用super调用父类的构造函数! this.grade=等级; } myGrade(){ alert('我在年级'+this.grade); } } 注意PrimaryStudent的定义也是通过class关键字实现的,extends表示原型链对象来自Student.子类的构造函数可能与父类的构造函数不同。比如PrimaryStudent需要name和grade作为两个参数,需要用super(name)调用父类的构造函数,否则无法正常初始化父类的name属性。 PrimaryStudent已经自动获取了父类Student的hello方法,我们在子类中定义了一个新的myGrade方法。 ES6引入的类和原来的JavaScript原型继承有什么区别?事实上,它们之间没有区别。class的作用是让JavaScript引擎实现我们原本需要编写的原型链码。总之,使用class的好处就是大大简化了原型链码。
