JavaScript 是一门动态语言,而且它没有类的概念。ES6 新增了class 关键字,但只是语法糖,JavaScript 仍旧是基于原型。
涉及到继承这一块,Javascript 只有一种结构,那就是:对象。在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链(prototype chain),大家也可以简单理解为继承链(不精确)。
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依此层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。现在我们讨论一下继承实现的几种方式:
1. 使用构造函数
function Father(name, age) {
this.name = name;
this.age = age;
Father.prototype.sayHello = function sayHello() {
console.log("hello ,my name is " + this.name);
};
}
var father1 = new Father("zhangsan", "50");
function Son() {
this.name = "lisi";
}
Son.prototype = father1;
Son.prototype.getFatherName = function() {
return Son.prototype.name;
};
Son.prototype.hobby = "game";
var son1 = new Son();
console.log(son1); //Father { name: 'lisi' }
console.log(son1.name); //lisi
console.log(son1.getFatherName()); //zhangsan
console.log(son1.hobby); //game
console.log(son1.age); //50
son1.sayHello(); //hello ,my name is lisi
上述代码中我们需要留意的是设定子类的原型这句话的位置,上例中就是Son.prototype = father1;
这句话,这句话建立了Son和Father的链接。为什么要关心这句话在代码中的位置呢?我们可以考虑一下,如果把var son1 = new Son()
这句话放到Son.prototype = father1;
前面会怎么样?
每个对象都有一个默认的原型,我们在使用继承的时候会把原型修改为我们想要的父类,所以如果按照上述方法修改我们会发现father1里面的属性我们是无法访问的,返回undefined,因为此时son1的原型指向默认的原型也就是Object。与之类似的是如果把Son.prototype = father1;
和Son.prototype.hobby = "game";
等语句交换位置,我们可以得到类似的结果,hobby将无法访问。因为__proto__
属性过时,所以不建议大家使用__proto__
以后大家在有这种方法创建继承关系的对象时需要注意设定原型的时机,在平时使用的过程中我们还可以将father1改为字面量对象,这样代码就更加简洁。这里使用function主要是理解子类的原型其实就是一个具体的对象,不要把原型理解的太过复杂。不过如代码中看到的那样,子类的原型并不完全等于父类对象,我们可以在子类的原型中继续添加属性,这些熟悉也可以被子类的对象继承,但不属于父类对象的属性。
2. 使用Object.create
ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数,第二个参数是对象自己特有的属性或者要覆盖原型的属性。第二个参数的格式和Object.defineProperties
定义属性是相同的,如果不明白可以参考上篇关于对象创建的博客。
var father1 = {
name:"zhangsan",
age:50,
sayHello:function(){
console.log("hello, my name is "+ this.name);
}
};
var son1 = Object.create(father1,{
name:{value:"lisi"},
getFatherName:{value:function(){return father1.name;}}
});
console.log(son1.name);
console.log(son1.getFatherName());
console.log(son1.age);
son1.sayHello();
这种创建方式和上述比起来代码更容易理解,有一个不太好的地方在于,如果需要son2,son3那么就会需要很多的类似代码,这种情况我们也可以利用工厂模式将子类的代码封装成函数进一步改善Object.create的创建。不懂工厂模式还是参考上篇对象创建的博客。
3. 使用class
ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不一样的。 JavaScript 仍然是基于原型的。这些新的关键字包括 class, constructor, static, extends, 和 super。
class Father{
constructor(){
this.name = "zhangsan";
this.age = 50;
}
sayHello(){
console.log(this.name);
}
}
class Son extends Father{
constructor(name){
super();
this.name = name;
}
get getFatherName (){
return new Father().name;
}
}
var son1 = new Son("lisi");
console.log(son1.name); //lisi
console.log(son1.getFatherName); //zhangsan
console.log(son1.age); //50
son1.sayHello(); //lisi
我们从上述代码可以发现,子类中的name覆盖了父类中的name属性,我们在子类中无法通过super等调用到父类的name。当然class还有一些其他的特点,比如类声明和函数声明不同,函数声明存在变量提升现象,而类声明不会, 类体中的代码都强制在 严格模式 中执行,即便你没有写 "use strict"等等特性。如果你对这部分有兴趣可以点这里。