深入研究js构造函数和原型

时间:2021-09-29 17:49:21

很快就要从新浪离职了,最近心情比较轻松,抽点空整理一下构造函数和原型的机理。

我们都知道,在经典设计模式中我们最常用的就是工厂模式、构造函数模式、原型模式这几种,听起来‘模式’好像很高大上的样子,实际上我们在日常写js代码中会经常用到。

简单的说,工厂模式就是我们平常写的一个个函数,当要完成某项功能时,就一遍一遍调用这个函数。如:

function add(a,b){
return a+b;
}

这是一个很简单的代码,完成的是一个很简单的两数相加的功能,当我们需要两数相加的计算时,调用这个函数就可以了。

我要在这里申明:工厂模式不一定比构造函数模式低级,因为用的场合不一样。富士康是全球最大的代工工厂,但我们没法说它很low,它的工厂流水模式也是非常先进的。

但是工厂模式无法实现实例识别的问题(太晦涩,不好懂),简单的说就是无法实现java、c#等类的功能。当然js没有类,只是用构造函数模拟类。创建一个类,然后功能相同或相近的都调用这个类,产生不同的实例,这样是不是引用起来就更方便一些呢?

在什么情况下用一般函数就可以,什么时候用类(构造函数)比较好呢?这个我认为没有定论。当实现一个很简单的功能的时候,如上面的这个相加函数,就没有必要变为构造函数模式。但是当我们要封装一个大的模块,比如手机滚动图等比较复杂的功能的时候,用构造函数就相对比较好一些,因为封装成类了,可以保持很好的封装性,更符合面向对象的特性。

这里就不把设计模式往细里谈了,拉回正题。

构造函数我们都知道如何构造,那么当new一个实例的时候,背后发生了什么呢?先看一个构造函数

function Person(name,age,gender){
this.name=name;
this.age = age;
this.gender=gender;
this.work = function(){
    alert(this.name);
}
} var person1 = new Person('zhangsan',24,'male');

那么person1这个实例是个什么?函数?对象?

如果console.log(person1) 就可以很清晰的看到,是一个对象{‘name':'zhangsan',age:24,gender:'male',work:function(){alert(this.name)}};

如果再创建一个person2,那么person2里的属性值和person1里的属性值是完全独立的,这样是不是我们就可以通过实例对象进行进一步的操作了呢?

但是这样定义还是有待商榷的,因为每次创建一个实例,都会让构造函数执行一次,所以每个实例里都有相同的work函数定义。这样既耗内存又没有任何必要。所以需要把这个构造函数改造如下:

function Person(name,age,gender){
this.name=name;
this.age = age;
this.gender=gender;
this.work =work;
}
function work(){
alert(this.name);
}
var person1 = new Person('zhangsan',24,'male');

大家看这样是不是就好很多了呢?

但是,这样也不是最优的,因为这个work函数在全局中,但是它的用途只是给Person这个构造函数用的,所以缺点就是1、暴露在全局中 2、没有封装

于是可以利用函数的原型。注意两个概念:函数天生有prototype属性,但是普通对象并没有。普通对象有的是_ _ proto_ _属性,所以实例对象通过__proto__属性可以访问到函数的prototype属性。这也就是原型链的一环。原型链后面再说。

运用prototype可以很好的解决构造函数的上述两个缺点。如下

function Person(name,age,gender){
this.name=name;
this.age = age;
this.gender=gender; }
Person.prototype={
work:function(){
alert(this.name);
}
}
var person1 = new Person('zhangsan',24,'male');

大家看,这样是不是更加优化,更加有面向对象的思想了呢?

原型的由来和好处说完了,再说说上面提到的原型链。

实例person1的__prototype__属性可以访问Person的prototype属性,所以可以正确的解析work函数。这就是原型链。具体来说就是:

person1要寻找属性时,首先看自己有没有这个属性。(不要忘了person1就是一个对象),如果没有,好,那就往Person的protype上找。找到了,然后执行就可以了。

这里注意寻找的顺序: 自身----》构造函数的Prototype---》Object的prototype,如果再没有,就报错了。

那么我们如果要判断一个实例的某个属性是类上的(构造函数)还是类的原型上的呢?这里有in操作符合hasOwnProperty方法来定位。

为什么要判断呢?因为有的需求可能只要实例所在的类上的属性,不要原型上的属性。那么这时这两个操作符/方法就派上大用场了

function Person(name,age,gender){
this.name=name;
this.age = age;
this.gender=gender; }
Person.prototype={
work:function(){
alert(this.name);
}
} var person1 = new Person('zhangsan',24,'male');
console.log(person1.hasOwnProperty('name')) ; //true
console.log('name' in person1); //true

如果hasOwnProperty返回的是true,那么说明name这时属性是类上的。in操作符返回的是true,说明name这个属性肯定是类上的或者类的原型上的。

通过hasOwnProperty和in就可以定位确定啦。

我遇到过一个面试题,是考察构造函数和原型的,大概是这样:

function Person(name,age,gender){
this.name=name;
this.age = age;
this.gender=gender; }
Person.prototype={
work:function(){
alert(this.name);
}
} var person1 = new Person('lisi',34,'male');
Person.study= 'English';
Person.prototype.course='Math'; console.log(person1.study);
console.log(person1.course);

ok,看看console.log(person1.study);返回的是什么呢?再看看console.log(person1.course);返回的是什么呢?

呵~呵~

答案是 undefined 和Math

这又是为什么呢?这就涉及到原型的动态性。什么意思?也就是说原型上的属性是动态性的,虽然创建实例在前面,但是当在下面再创建prototype上的属性的时候,prototype会自动更新。

但是构造函数没有动态性,定义了Person类以后再给Person这个类添加属性或方法是不会添加成功的。

要深究起来,构造函数和原型这块确实是水很深,坑不少,需要我们认真研究,不拘泥于效果出来就万事大吉。

先写到这里啦!