原型链
原型是JavaScript面向对象编程中非常重要的概念。明白原型链就是去搞清楚prototype和__proto__的关系。
举个例子说明prototype对象:
//构造函数MyClass
function MyClass() {
...
}
var mc = new MyClass(); //实例mc
var mc2 = new MyClass(); //实例mc2
var mc3 = new MyClass(); //实例mc3
MyClass.prototype.a = 123;
可以看出原型对象相当于一个公共的对象区域,通过同一个构造函数创建的实例对象可以访问到相同的原型对象。
然后,再来说一说原型链。下面就是一个原型链,能看懂这个基本这块知识就没什么问题了。
继承
1.原型链
原型链继承基本思想就是让继承的原型对象指向被继承构造函数的实例。
function Parent() {
this.name = "parent";
}
Parent.prototype.getName = function () {
return this.name;
};
function Children(name) {
this.cName = "children";
}
// 通过原型链实现继承Children.prototype.__proto__ === Parent.prototype
Children.prototype = new Parent();
//重写方法一定要写在继承之后不然会被覆盖
Children.prototype.getName = function () {
return this.cName;
};
var c = new Children();
console.log(c.getName()); //children
代码定义了两个构造函数Parent和Children,Children继承了Parent,并且是通过创建Parent的实例赋给Children.prototype实现继承。可以看出原型链继承的本质就是重写构造函数的原型对象,这样存在Parent的实例中的所有属性和方法也能通过Children.prototype去访问了。
缺点:
-
继承的子类原型上的方法创建一定要写在原型链继承之后,不然会被父类实例覆盖。
-
单纯的使用原型链继承,主要问题来自包含引用类型的原型。
function Parent(){
this.colors = ['parent'];
}
function Children(){
}
Children.prototype = new Parent();
var c1 = new Children();
var c2 = new Children();
c1.colors.push('children');
console.log(c1.colors, c2.colors); //[ 'parent', 'children' ] [ 'parent', 'children' ]
在Parent构造函数中定义了一个color属性,当Children通过原型链继承后,这个属性就会在Children.prototype中,导致Children里的所有实例都会共享这个属性。所以c1修改了colors这个引用类型值也会反映到c2中。
2. 构造函数
此方法为了解决原型中包含引用类型值所带来的问题。这种方法的思想就是在子类构造函数的内部调用父类构造函数,主要是借助apply()和call()方法来改变对象的执行上下文。
function Parent() {
this.colors = ["parent"];
}
function Children() {
Parent.call(this);
}
var c1 = new Children();
var c2 = new Children();
c1.colors.push("children");
console.log(c1.colors, c2.colors); //[ 'parent', 'children' ] [ 'parent' ]
call方法还可以传递参数,进一步修改。
function Parent(color){
this.color = color;
}
function Children(color, name){
Parent.call(this, color);//继承Parent的属性
this.name = name;
}
var c1 = new Children('red', 'c1');
console.log(c1.color); //red
在new一个实例时,new里面的运行过程会使this指向新的实例对象,所以在Children里添加的语句让继承的Person属性的this指向当前的实例,那么每创建一个实例都会产生属于自己的colors属性了。而且借用构造函数还可以传递参数。
但是这种方法不能够继承方法,只能继承属性。所以没有办法实现函数复用。
3.组合继承(原型链+构造函数)
组合继承就是1和2的结合,这么看来思路就很简单了。
组合继承:使用原型链实现对原型属性和方法的继承,然后通过借用构造函数实现对实例属性的继承。
function Parent(name) {
this.name = name;
this.arr = ["parent"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Children(name) {
Parent.call(this, name); //1.继承属性
this.color = 'red';
}
// 2.继承方法
Children.prototype = new Parent();
// 让原型中的constructor指向函数自己
Children.prototype.constructor = Children;
//重写方法一定要写在继承之后不然会被覆盖
Children.prototype.getValue = function () {
return this.color;
};
var c1 = new Children('c1');
var c2 = new Children('c2');
c1.arr.push('c1');
console.log(c1.arr, c2.arr); //[ 'parent', 'c1' ] [ 'parent' ]
console.log(c1.name, c2.name);//c1 c2
这种模式避免了原型链和构造函数继承的缺陷,融合了他们的优点,是最常用的一种继承模式。
4.原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function p(obj) {
function Fn() {}
Fn.prototype = obj; //注意:这里是赋值的是对象的引用
return new Fn();
}
在p函数内部,先创建一个临时的构造函数然后将传入的对象作为这个构造函数的原型,最后返回这个临时构造函数的实例。这个时候的实例就和传入的对象建立了原型链。
var person = {
name: 'xiaoming',
friends: ['xiaohong', 'xiaohua']
};
var onePerson = p(person); //onePerson.__proto__ = Fn.prototype = person
onePerson.friends.push('a');
console.log(onePerson.friends, person.friends);//['xiaohong', 'xiaohua', 'a'] ['xiaohong', 'xiaohua', 'a']
这种模式中必须有一个对象作为另一个对象的基础。需要注意的是,赋值时将对象的引用赋给临时构造函数的原型,相当于p函数对传入obj对象进行了一次浅拷贝。这意味着onePerson修改数组friends时,对象person里的friends也会变化。
解决方法:Object.create()方法。
ES5通过Object.create()方法规范了原型式继承,作用和上面的p一样。但是更加简洁。
var person = {
name: 'xiaoming',
friends: ['xiaohong', 'xiaohua']
};
var onePerson = Object.create(person); //与p函数的作用一样
onePerson.friends.push('a');
console.log(onePerson.friends, person.friends);//['xiaohong', 'xiaohua', 'a'] ['xiaohong', 'xiaohua', 'a']
5.寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数。
function createFn(obj){
var clone = Object.create(obj);
clone.sayHi = function () {
console.log('Hi');
};
return clone;
}
var person = { name: 'xiaoming'};
var onePerson = createFn(person);
onePerson.sayHi(); //Hi
基于person返回一个新对象onePerson,新对象不仅拥有person的属性和方法还有自己的sayHi方法。在对象不是自定义类型和构造函数的情况下,这个是一个有用的继承模式。
5.寄生式组合继承
在3中所讲的**组合继承(原型链+构造函数)**中,继承的时候需要调用两次父构造函数。
第一次在子构造函数中:Parent.call(this, name);第二次在子构造函数的原型指向父类的实例时:Children.prototype = new Parent()。
因为调用了两次在使用var c = new Children()时候会产生两组name和color属性。一组在Children实例上,一组在Children原型上。只不过实例寻找属性因为自己拥有不会去原型上找,因此不会带来异常。
使用寄生式组合继承可以规避这个问题。
基本思路:不必为了设置子类型的原型而调用父类的构造函数。我们需要的无非就是父类原型的一个副本。
本质上就是使用寄生式继承来继承父类的原型,然后将结果指定给子类型的原型。
//继承方法
function inheritPrototype(children, parent){
//相当于实现prototype.__proto__===parent.prototype
var prototype = Object.create(parent.prototype);
prototype.constructor = children;
children.prototype = prototype;
}
第一步创建父类原型的副本,第二步将创建的副本添加constructor属性,第三部将子类的原型指向这个副本。
// 组合继承中继承方法的部分替换
function Parent(name) {
this.name = name;
this.arr = ["parent"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Children(name) {
Parent.call(this, name); //1.继承属性
this.color = 'red';
}
// 2.继承方法
//Children.prototype = new Parent();
// 让原型中的constructor指向函数自己
//Children.prototype.constructor = Children;
inheritPrototype(Children, Parent);
//重写方法一定要写在继承之后不然会被覆盖
Children.prototype.getValue = function () {
return this.color;
};
var c1 = new Children('c1');
var c2 = new Children('c2');
c1.arr.push('c1');
console.log(c1.arr, c2.arr); //[ 'parent', 'c1' ] [ 'parent' ]
console.log(c1.name, c2.name);//c1 c2
也可以把inheritPrototype函数拆开写。
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
在ES6中新增了一个方法Object.setPrototypeOf,是上面两行代码的结合体,可以直接解决constructor属性的关联问题。
Object.setPrototypeOf(Children.prototype, Parent.prototype);
console.log(Children.prototype.constructor === Children); //true
👌,这就是继承的所有方式,面试的时候写setPrototypeOf还是稍微略秀😁。