一文让你搞懂javascript如何实现继承
一、本文想给你聊的东西包含一下几个方面:(仅限于es6之前的语法哈,因为es6里面class这关键字用上了。。)
1.原型是啥?原型链是啥?
2.继承的通用概念。
3.Javascript实现继承的方式有哪些?
二、原型是啥?原型链是啥?
1.原型是函数本身的prototype属性。
首先js和java不一样,js顶多算是一个基于对象的语言,而不是标准的面向对象的语言。
所以我们谈继承,只能是基于new关键字作用域构造函数的场景。
上代码:
function Person(name,age) { this.name = name; this.age = age; } console.log(Person.prototype);
代码 1
图1
定义一个构造函数,默认起原型就是一个Object对象,相当于一个new Object()。
而且还有一个new出来的对象有一个隐式原型属性__proto__,也指向了构造函数的原型。
也就是说: Person.prototype === new Person().__proto__。
用图来表示就是:
图2
在上图中,我把Object.prototype 叫rootObject,那么rootObject中就有了所有对象都共享的方法,如下图:
图3
如果person.toString()方法调用,那么起自身没有toString方法,就是走__proto__指向的原型对象中找,而object中也没有
所有就找到了根对象。所以构造函数原型对象存在的意义是使得该构造函数产生的对象可以共享属性和方法。
所以原型对象中的属性和方法就类似于java类中定义的static属性和方法,所有对象都可以共享。
那么如上图2所示,person -> object -> rootObject之间就形成了原型链。
二、继承的通用概念
如果一个类B继承了类A,在java中这些写:class B extends A{}
那么类B就拥有了A中的所有属性和方法。
继承是面向对象编程的一大特性,目的很简单,就是复用。
三、javascript中实现继承的方式有哪些?
1.原型链
假如有个构造函数Student想要继承Person函数,想拥有Person中的属性和方法,可以使用原型链来实现。
上代码
// 定义Person
function Person(name,age) {
// 保证属性有初始值
this.name = name ? name : "";
this.age = (age || age === 0) ? age : 0;
this.setName = function (name) {
this.name = name;
}
this.setAge = function (age) {
this.age = age;
}
this.getPersonInfo = function () {
return "[Person]: " + this.name + "_" + this.age;
}
}
// 定义一个所有Person对象都能共享的属性和方法
Person.prototype.typeDesc = "人类";
Person.prototype.hello = function () {
console.log("hello");
}
function Student(score) {
this.score = score;
this.setScore = function (score) {
this.score = score;
}
this.getStudentInfo = function () {
return "[Student:]: " + this.score;
}
}
// 修改Student的原型
Student.prototype = new Person();
let student1 = new Student(90);
let student2 = new Student(80);
let student3 = new Student(70);
console.log(student1.typeDesc); // 能访问
student1.setName("aa");
student1.setAge(99);
console.log(student1.getPersonInfo()); // 能访问
console.log(student1.getStudentInfo()); // 能访问
代码2
给你一张图吧 更清楚
图 4
老铁,你思考下?虽然看似student1对象能访问了能访问了Person中定义的方法和属性,但是有没有问题呢?
本来name,age是对象的私有属性,不属于“类级别”,但是他们却出现在了Student的原型对象中,而且此时如果你
console.log(student2.name),发现其访问到了原型person对象的name属性了,是个初始的空字符串,这里为什么要在Person函数中使用初始值,
这个在工作中是很常见的,对象创建出来一般属性都是需要初始值的。
所以原型链实现继承,缺点是:原型对象中多出了一些没必要的属性。
而且由于student2和student3等其他Student的对象仍然能访问到原型对象person中的属性,这会让人产生错觉,以为他们也拥有name,age的私有属性。
于是,你接着看下面的方式。
2.复用构造方法
这东西严格来讲,我感觉不太像继承,但是好像用起来还挺好用,起码省事了。。。。
继续哈,上代码啊(改变一下代码2)
// 定义Person function Person(name,age) { // 保证属性有初始值 this.name = name ? name : ""; this.age = (age || age === 0) ? age : 0; this.setName = function (name) { this.name = name; } this.setAge = function (age) { this.age = age; } this.getPersonInfo = function () { return "[Person]: " + this.name + "_" + this.age; } } // 定义一个所有Person对象都能共享的属性和方法 Person.prototype.typeDesc = "人类"; Person.prototype.hello = function () { console.log("hello"); } function Student(name, age, score) { // 使用call调用函数,可以改变this指向,服用了父类的构造方法 Person.call(this, name,age); this.score = score; this.setScore = function (score) { this.score = score; } this.getStudentInfo = function () { return "[Student:]: " + this.score; } } let student1 = new Student("aa", 99, 99); console.log(student1.typeDesc); // undefined console.log(student1.hello); // undefined console.log(student1.getStudentInfo()); // 能访问 console.log(student1.getPersonInfo()); // 能访问
代码 3
此时虽然,虽然复用了Person构造函数,但是原型Person的原型student1无法访问到。
缺点很明显:虽然复用了Person的构造函数,但是却没有继承Person的原型。
好了,我们演变一下。。
3.共享原型
基于上述代码3,在Student函数后面加入如下代码:
Student.prototype = Person.prototype;
代码 4
其实就是两个构造函数都指向同一原型。。
此时发现,student1能访问Person原型上的内容了。
还是要问一下,这样就行了吗?
问题:一旦Student向原型里面加了变量或者函数,或者修改原型中的变量内容时,哪怕是Person构造出来的对象,
同样也感知到了。。。。 这样互相影响的话,两个构造函数的原型中的变量和函数掺杂在一起,确实不合适?
那怎么办呢?
来吧,看看下面的变种。
4.圣杯模式
说实话我也不知道为啥取名叫圣杯模式,感觉也不是官方的命名,有些人还叫其他名字。
把代码4替换成如下代码:
// 定义空函数 function F() {} // 空函数和Person共享原型 F.prototype = Person.prototype; // 改变Student的原型 Student.prototype = new F(); // 添加原型上的构造函数 Student.prototype.constructor = Student;
代码 5
这样做Student的原型和Person的原型就不是一个对象了,而且不像原型链那样,由于new Person()作为Student.prototype导致该原型对象中包含了Person对象的私有属性。
来吧,给你个最终版本的代码,希望能帮助到你,能力有限,相互借鉴哈。。
5.圣杯模式+复用构造函数(算是比较完美了)
// 定义Person function Person(name,age) { // 保证属性有初始值 this.name = name ? name : ""; this.age = (age || age === 0) ? age : 0; this.setName = function (name) { this.name = name; } this.setAge = function (age) { this.age = age; } this.getPersonInfo = function () { return "[Person]: " + this.name + "_" + this.age; } } // 定义一个所有Person对象都能共享的属性和方法 Person.prototype.typeDesc = "人类"; Person.prototype.hello = function () { console.log("hello"); } function Student(name, age, score) { // 使用call调用函数,可以改变this指向,服用了父类的构造方法 Person.call(this, name,age); this.score = score; this.setScore = function (score) { this.score = score; } this.getStudentInfo = function () { return "[Student:]: " + this.score; } } // 定义空函数 function F() {} // 空函数和Person共享原型 F.prototype = Person.prototype; // 改变Student的原型 Student.prototype = new F(); // 添加原型上的构造函数 Student.prototype.constructor = Student; let student1 = new Student("aa", 99, 99); console.log(student1.typeDesc); // 人类 student1.hello(); // hello console.log(student1.getStudentInfo()); // 能访问 console.log(student1.getPersonInfo()); // 能访问 let student2 = new Student("bb", 33, 88); student2.setScore(89); // student2和student1都各自有自己的私有属性,并不会受影响。 console.log(student1.getStudentInfo()); console.log(student2.getStudentInfo()); Student.prototype.temp = "新加属性"; console.log(Person.prototype.temp); // undefined
代码 6
总结:可能我们在平常工作中很少这样写代码,或者用到这种继承模式,但是框架中很有可能会用到这些思想。
圣杯模式是共享原型模式的一个变种,使用空函数F来作为中间桥梁,巧妙得解决了共享原型模式的问题,同时
也解决了原型链模式的产生多余属性的问题。