一、原型基础
在之前的文章:《JavaScript创建对象之单例、工厂、构造函数模式》中详细介绍了构造函数模式创建对象的方式,构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的(实例识别)。
但是利用构造函数模式创建出来的每个对象,都拥有一份自己的属性和方法,拥有自己的属性是无可厚非的,但是方法应该是要共有的,不应该每个实例都有一份,每个对象都拥有一份方法的话,也会多占用内存空间。
于是基于构造函数的原型模式就有了,原型模式解决了方法或者属性不能共有的问题,在原型模式中,把实例之间相同的属性和方法提取成共有的属性和方法,即:想让谁共有,就把它放在类.prototype
上。
function CreateJsPerson(name, age) {
this.name = name; // p1.name=name
this.age = age;
}
CreateJsPerson.prototype.writeJs = function () {
console.log(this.name + ' write js');
};
var p1 = new CreateJsPerson('iceman' , 25);
var p2 = new CreateJsPerson('mengzhe' , 27);
console.log(p1.writeJs === p2.writeJs); // true
有三个非常重要的特性:
每一个函数数据类型(普通函数、类)都有一个自带的属性:
prototype
(原型),并且这个属性是一个对象数据类型的值;在
prototype
上浏览器天生给它加了一个属性:constructor
(构造函数),属性值是当前函数(类)本身;每一个对象数据类型(普通的对象、实例、prototype...)也天生自带一个属性:
__proto__
,属性值是当前实例所属的原型(prototype
)。
看完以上三句话,是不是有些想吐了呢?哈哈,刚接触的时候都会感到一头雾水,接下来会慢慢讲解。但是别问为什么会有这三个结论,这都是浏览器自带的哦!
再看一个例子:
function Fn() {
this.x = 100;
this.sum = function () {}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.sum = function () {
};
var f1 = new Fn();
var f2 = new Fn();
console.log(Fn.prototype.constructor === Fn); // true
下图为该例子对应的图解(为了不增加难度,只画堆内存),注意联系上面的三个结论来理解哦:
Object是JavaScript中所有数据类型的基类(最顶层的类):
-
f1 instanceof Object
输出true,因为f1通__proto__
,可以向上级查找,不管多少级,最后总能找到Object; - 因为是最顶层的类了,所以
Object.prototype
上没有__proto__
这个属性;
二、原型链模式
f1.hasOwnProperty('x')
,f1能调用hasOwnProperty,那么hasOwnProperty是f1的一个属性。但是我们发现f1的私有属性上并没有这个方法,那如何处理的呢:
通过 对象名.属性名 的方式获取属性值的时候,首先在对象的私有属性上进行查找,如果私有的属性中存在这个属性,则获取的是私有的属性值;
如果私有的属性中没有,则通过
__proto__
找到所属类的原型(类的原型上定义的属性和方法都是当前实例的公有的属性和方法),原型上存在的话,获取的是原型上公有的属性值;如果原型上也没有,则继续通过原型上的
__proto__
继续向上查找,一直找到Object.prototype
为止。
以上的这种查找机制,就是原型链模式。
console.log(f1.getX == f2.getX); // true
console.log(f1.__proto__.getX == f2.__proto__.getX); // true
console.log(f1.getX === Fn.prototype.getX); // true
console.log(f1.sum === f2.prototype.sum); // false
console.log(f1.sum === Fn.prototype.sum); // false
注意:在IE浏览器中,原型模式也是这个原理,但是IE浏览器怕你通过__proto__
把公有的修改,禁止我们使用__proto__
。
三、原型模式中的this
在原型模式中,this常见的情况有两种:
在类中:
this.xxx = xxx;
this表示当前类的实例;-
在某一个方法中:要看"."前面是谁this就是谁,通过以下的三个步骤:
- 需要先确定this的指向(this是谁);
- 把this替换成对应的代码;
- 按照原型链查找的机制,一步步的查找结果;
function Fn() {
this.x = 100;
this.y = 200;
this.getY = function () {
console.log(this.y);
}
}
Fn.prototype = {
constructor:Fn,
y:300,
getX : function () {
console.log(this.x);
},
getY : function () {
console.log(this.y);
}
}
var f = new Fn;
f.getX(); // --> console.log(f.x) --> 100
f.__proto__.getX(); // --> this是f.__proto__ --> console.log(f.__proto__.x) --> undefined
Fn.prototype.getX(); // --> undefined
f.getY(); // --> 200
f.__proto__.getY(); // --> 300
四、在内置类的原型上扩展方法
在Array类的原型上扩展一个去重的方法:
Array.prototype.myUnique = function () {
var obj = {};
for (var i = 0; i < this.length; i++) {
var cur = this[i];
if(obj[cur] == cur) {
this[i] = this[this.length - 1];
this.length --;
i--;
continue;
}
obj[cur] = cur;
}
obj = null;
return this; // 返回this目的是为了实现链式写法
};
var ary = [12, 23, 23, 13, 12, 13, 23, 13];
ary.myUnique();
console.log(ary);
ary.myUnique().sort(function (a, b) {
return a - b;
});
console.log(ary);
Array.prototype.myUnique(); // this --> Array.prototype
链式写法:执行完数组的一个方法可以紧接着执行下一个方法(jQuery中实现了链式写法)。
ary.sort(function (a, b) {
return a - b;
}).reverse().pop();
console.log(ary);
ary为什么可以使用sort方法呢?因为sort是Array.prototype上的公有方法,而数组ary是Array这个类的一个实例,所以ary可以使用sort方法,也就是数组才能使用Array原型上定义的属性和方法;
sort执行完成的返回值是一个排序后数组,可以继续执行reverse;
reverse执行完成的返回值是一个数组,可以继续执行pop;
pop执行完成的返回值是被删除的那个元素,不是一个数组了,所以再执行push会报错。
五、批量设置原型上的公有属性和方法
5.1、为原有函数的prototype起一个别名
function Fn() {
this.x = 100;
}
var pro = Fn.prototype; // 把原来原型指向的地址赋值给我们的pro,现在它们操作的是同一个内存空间
pro.getX = function () {
};
pro.getY = function () {
};
var f1 = new Fn();
jQuery中就是这么实现的。
5.2、重构原型对象的方式
自己新开辟一个新内存,存储我们公有的属性和方法,把浏览器原来给Fn.rototype开辟的那个替换掉:
function Fn() {
this.x = 100;
}
Fn.prototype = {
constructor:Fn,
a:function () {
},
b:function () {
}
};
var f = new Fn;
只有浏览器天生给Fn.prototype
开辟的堆内存里面才有constructor
,而我们自己开辟的这个堆内存没有这个属性,这样constructor
指向的就不是Fn
而是Object
。
console.log(f.constructor); // --> 没做处理之前输出 Object
为了和原来的保持一致,我们需要手动的增加constructor
的指向:
constructor:Fn
注意:不能将这种方式用于给内置类增加公有的属性,例如:
Array.prototype = {
constructor:Fn,
myUnique:function () {
}
};
console.dir(Array.prototype);
因为如果这种方式能用于内置类的话,会将之前内置类中已经存在于原型上的属性和方法给替换掉,所以浏览器是屏蔽这种方式修改内置类的。
所以如果想给内置类增加公有方法的话,应该使用如下方式:
Array.prototype.myUnique = function () {
};
但是这种方式也是有危险的,因为我们可以一个一个的修改内置类的方法,当通过以下的方式在数组的原型上增加方法,如果方法名和原来内置的方法名重复,会把内置类内置的公有方法修改掉,所以以后在内置类的原型上增加方法的时候,命名都需要加特殊的前缀。
Array.prototype.sort = function () {
// .....
};
六、继承
6.1、原型继承
function A() {
this.x = 100;
}
A.prototype.getX = function () {
console.log(this.x);
};
function B() {
this.x = 200;
};
B.prototype = new A();
var n = new B;
原型继承是JavaScript中最常用的一种方式。
子类B想要继承父类A中的所有的属性和方法(私有+公有),只需要让B.prototype = new A;
即可。
原型继承的特点:它是把父类中私有的+公有的都继承到了子类的原型上(子类公有的)。
核心:原型继承,并不是把父类中的属性和方法克隆一份一模一样的给B,而是让B和A之间增加了原型链的连接,以后B的实例n想要用A中的getX方法,需要一级级的向上查找来使用。
6.2、call继承
function A() {
this.x = 100;
}
A.prototype.getX = function () {
console.log(this.x);
}
function B() {
// this --> n
A.call(this); // --> A.call(n) 把A执行,让A中的this变为了n
}
var n = new B;
console.log(n.x);;
call继承是把父类私有的属性和方法,克隆一份一模一样的作为自己的私有的属性,注意:只有私有的属性和方法才能继承。公有的属性和方法是没法继承的。所以如果执行n.getX()
会报错:
Uncaught TypeError: n.getX is not a function
6.3、冒充对象继承
function A() {
this.x = 100;
}
A.prototype.getX = function () {
console.log(this.x);
}
function B() {
// this --> n
var temp = new A;
for (var key in temp) {
this[key] = temp[key];
}
temp = null;
}
var n = new B;
console.log(n.x);;
冒充对象继承会把父类 私有的+公有的 都克隆一份一模一样的给子类私有的。
6.4、混合模式继承
function A() {
this.x = 100;
}
A.prototype.getX = function () {
console.log(this.x);
}
function B() {
A.call(this); // --> n.x = 100;
}
B.prototype = new A; // --> B.prototype: x=100 getX
B.prototype.constructor = B;
var n = new B;
n.getX();
混合模式继承就是:原型继承+call继承。
使用混合模式继承可以让子类即拥有父类私有的属性和方法(call继承的特点),又拥有父类公有的属性和方法(原型继承的特点),但是有一个问题是,父类中私有属性也会成为子类的公有属性,比如本例中的B类中,在私有属性和原型上都拥有一个x=100
,虽然根据原型链搜索原则,在使用的没有影响,但是作为有一个代码洁癖的程序员还是觉得不妥,因为毕竟是占用了那么一丢丢的空间(哈哈),那么这时候就可以看接下来的寄生组合式继承了。
6.5、寄生组合式继承
function A() {
this.x = 100;
}
A.prototype.getX = function () {
console.log(this.x);
}
function B() {
A.call(this);
}
//B.prototype = Object.create(A.prototype); // IE6、7、8不兼容
B.prototype = objectCreate(A.prototype);
B.prototype.constructor = B;
var n = new B;
console.dir(n);
function objectCreate(o) {
function fn() {}
fn.prototype = o;
return new fn;
}
6.6、中间类继承
首先声明,这种中间类继承是不兼容的,但是可以用于移动端,因为移动端不用兼容IE,并且这种方式在大多数书中都没有介绍,算是一种奇技淫巧吧。
function avgFn() {
arguments.__proto__ = Array.prototype;
arguments.sort(function (a, b) {
return a-b;
})
arguments.pop();
arguments.shift();
return eval(arguments.join('+')) / arguments.length;
}
console.log(avgFn(10, 20, 30, 10, 30, 30, 40));
个人公众号(icemanFE):分享更多的前端技术和生活感悟