深入学习JavaScript的人都知道,JavaScript也是门支持面向对象编程的语言。面向对象的语言有一个标志,就是它们都有类的概念,通过类可以创建任意多个具有相同属性和方式的对象。然鹅,ECMAScript里没有类的概念,(ES6的class也只是一种语法糖,它仍然是基于原型的),但是JavaScript可以通过原型链机制为对象提供“继承”功能。
在了解原型链之前,我觉得了解一下JavaScript的继承设计思想,也就是设计JavaScript的大神的设计想法和故事,是挺有帮助的。
在我一开始学习原型和原型链的时候,我是挺混乱的,像prototype、_proto__、constructor等等,以及它们的各种指向,都把我弄晕了(二仙桥大叔这样说?)。
理解原型对象
在JavaScript中创建一个新的函数,就会自动为这个函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。当调用构造函数创建一个新实例后,该实例的内部也将包含一个指针(私有属性),指向构造函数的原型对象,称之为 _proto__。
可以捋一捋的。我以我的理解换种话说。JavaScript中所有的函数(除箭头函数外)都有一个prototype属性,且只有函数才有。原型的属性和方法,都能被构造函数的实例对象所共享。对象原型的真实值被存储在内部专用属性[[Prototype]]中。
function Person(){} Person.prototype.sayHello=()=>{console.log('hello');} let QQi = new Person(); let mom = new Person(); QQi.sayHello()//hello mom.sayHello()//hello 共享访问 delete Person.prototype.sayHello; //删除方法 QQi.sayHello();//报错
JavaScript中constructor存在于每一个函数的prototype属性中,其中保存了指向该函数的一个引用,也就是constructor指向了函数本身。当使用new关键字调用函数时,执行的是内部方法[[Construtor]]函数,它会创建一个实例,然后再执行函数体,将this绑定到实例上。要注意的是,箭头函数没有this,而是通过查找作用域链决定this值得,所以他不具有[[Constructor]]方法,不能被new。
console.log(Person.prototype.constructor);// ƒ Person(){}
console.log(Person.prototype.constructor === Person)//true
JavaScript中实例对象都有一个内置属性,即_proto__(隐式原型链属性),一般情况下它指向创建它的构造函数的prototype属性。函数作为对象,也有这个属性。请记住,实例的指针仅仅指向原型,而不指向构造函数。
console.log( QQi.__proto__)
console.log( Person.prototype)
在ES6中,引入了Super,可以简化原型访问,特别在多重继承下使用起来会感觉很妙。因为Super能保证指向正确的对象。(使用起来就感觉和Java中的super关键字用起来的感觉差不多?)
理解原型链
不过我们在进行页面开发或者写代码时,很少会主动写到prototype,它的主要作用就是当js引擎查找对象的某个属性时,先查找对象本身是否存在该属性,如果不存在,就会在原型链上一层一层网上查找。所以理解原型和原型链,有助于我们更好的理解这门语言,用好这门语言。
那么怎么专业地回答:“原型链是什么?怎么实现的?”——我们让原型对象等于另一个类型的实例,原型对象将包含一个指向另一个原型的指针,不断套娃,层层递进,就构成了实例与原型的链条。这就是所谓的原型链。
原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。
下面有个有关原型链的小例子,就是Person没有定义或者重写toString方法,仍然可以调用toString()方法,这个就是通过原型链,Person的原型是Object,Object有toString方法,那就找到了toString方法。
console.log(QQi.toString());//[object Object]
console.log(QQi.__proto__.__proto__.toString === QQi.toString);//true
原型链的终点是什么,毕竟一条链子不可能无限长。我们知道,JavaScript里,所有引用类型默认都继承Object,那么Object继承什么?还是Object就是终点?
console.log(Object.prototype.__proto__); //null
可以看到Object的原型的指针指向了null,那么null不可能再有指向了,那么null就是原型链的终点了。
继承模式
JavaScript的继承主要基于原型链。在JavaScript里有很多的继承模式,我也是学习和参考的《JavaScript高级程序设计语言》这本书,我将捋一捋组合继承,一种最常用的继承模式;寄生组合式继承,一个引用类型最理想的范式。
组合继承
组合指的是使用构造函数和原型模式来实现继承。构造函数定义实例属性,原型模式用于自定义方法和共享的属性。这种组合模式可以让每个实例有自己的一些特殊属性和方法,实例间不会互相影响,而通过原型可以让一些属性和方法供每个实例共享,实现复用,节省内存。
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function(){
constructor: Person,//创建新的函数时可能会把构造函数重写掉
console.log(this.name);
}
function Teacher(name,age) {
Person.call(this,name) //传参
this.age = age;
};
Teacher.prototype = new Person();//Person实例赋给Teacher原型
Teacher.prototype.constructor = Person;//因为它重写后constructoor就是Teacher了
Teacher.prototype.sayAge = function() {
console.log(this.age);
}
let cluo = new Teacher("cluo",28);
cluo.sayAge();//28
let zhaomeili = new Teacher("zhaomeili",30);
zhaomeili.sayName();//zhaomeili
像这样,cluo和zhaomeili都有自己的属性,但是都可以调用Teacher上定义的新方法,也可以调用从Person上继承来的方法。
寄生组合式继承
直接看代码,比一下上面的组合继承,变化的地方不多,就是寄生组合式继承写了一个函数,在函数里调用了超类构造函数,感觉思路还是和上面的差不多的。不过值得注意的式,整个“继承”过程,只调用了一次“父类”构造函数。像上面的组合继承,“儿子”调用构造函数还是会调用到“父亲”构造函数的: Person.call(this,name)。所以寄生组合式继承的效率会高一点。
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function(){
constructor: Person,//创建新的函数时可能会把构造函数重写掉
console.log(this.name);
}
function Teacher(name,age) {
Person.call(this,name) //传参
this.age = age;
};
function inheritPrototype(Teacher,Person){
let prototype = Object(Person.prototype) //创建父原型的一个副本对象
prototype.constructor = Teacher; //增强对象 为副本添加constructor属性
Teacher.prototype = prototype; //指定对象 将父本赋值给子类型
}
Teacher.prototype.sayAge = function() {
console.log(this.age);
}
总结
每次说到原型链,我脑海里就是一幅画面:一直小蜗牛,沿着大树的数干,一步一步努力往上爬哈哈哈。
其实刚开始看原型链,我就是被各种单词名词给弄混乱了。一定要记住:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。JavaScript的继承就基于原型链。