js高级程序设计理解

最近一直在看js高级程序设计这本书,一来想看这本书很久了,自己写前端代码这么久以来从来没有真正透彻地理解js,包括像this,还有闭包,原型这些概念都没有理解透彻。二来,思考了自己将来要走的路,前端是我必须要掌握的一门知识点。所以该苦其心志的时候就该苦。
废话少说,先来说说,继承。

一、继承

继承的模式有很多种,我觉得可以两两对比着看:

原型链 VS 借用构造函数

先来看一段原型链的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(){
this.superName = 'john';
this.friends = ['susan','smith'];
}
SuperType.prototype.saySuperName = function(){
alert(this.superName);
};
function SubType(){
this.subName = 'gong';
this.age = 14;
}
SubType.prototype = new SuperType();
var sub = new SubType();
sub.saySuperName(); //john
sub.friends.push('alex'); //'susan','smith','alex'
var sub2 = new SubType();
sub2.friends.push('david'); //'susan','smith','alex','david'

原型链的技巧在于将一个函数的原型指向另一个函数的实例上。继而创建了继承的关系。在创建子类的实例时,子类不仅拥有父类的属性,还能继承自父类原型上的方法。缺点在于所有子类的实例,内部都有一个原型指针,指向同一个父类的实例。这样会导致父类的引用类型的属性被所有子类共享。每一个子类没有单独属于自己的属性。
鉴于此,再来看一下借用构造函数技术:

1
2
3
4
5
6
7
8
9
10
function SuperType(){
this.colors = ['red','green'];
}
function SubType(){
SuperType.call(this);
}
var sub = new SubType();
sub.colors.push('yellow'); //'red','green','yellow'
var sub2 = new SubType();
sub2.colors.push('blue'); //'red','green','blue'

很显而易见,通过借用构造函数技巧,所有子类都拥有自己的属性了。总结技巧就在于在子类的构造函数中调用父类的构造函数。而这种设计模式的缺点在于每一个实例都有自己的属性以及方法。方法应该是通用的,所有实例共享,而单纯使用借用构造函数模式办不到,从这里看就可以看出原型链模式以及借用构造函数模式两者之间处于一个互补的关系,此消彼长。如果能够将两者的优点都结合在一起就好了。这就是我接下来要讲的

组合继承 VS 寄生组合式继承

组合继承就是将原型链和借用构造函数结合在一起的技术。先看一下下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(name){
this.name = name;
this.colors = ['red','green'];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name){
SuperType.call(this, name);
}
SubType.prototype = new SuperType();
var sub = new SubType('john');
sub.sayName() //'john'
sub.colors.push('yellow'); //'red','green','yellow'
var sub2 = new SubType('gong');
sub2.sayName() //'gong'
sub2.colors.push('blue'); //'red','green','blue'

可以看出组合继承很好地解决了前面两种模式各自的缺点,可以说是一种不错的设计模式。但是,它也有自己的不足之处。在只创建一个子类实例的情况下,这段代码一共调用了两次SuperType的构造函数,也就是说SuperType里面的属性会被创建两次。为了解决这个问题,我们先来看一下这段代码:

1
2
3
4
5
function object(o){
function F(){}
F.prototype = o;
return new F();
}

这段代码创建了一个临时构造函数F,然后将传入的对象作为构造函数的原型。最后返回这个构造函数的实例。先别管别的,再来看看下面这段代码:

1
2
3
4
5
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
subType.prototype = prototype; //指定对象
prototype.constructor = subType; //增强对象
}

以上这段代码首先创建了一个原型指向父类原型的实例。其次将子类原型指向该实例。最后将破坏了的子类结构重新整理,让实例的构造体指回子类函数。来对比一下以下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType(name){
this.name = name;
this.colors = ['red','green'];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name){
SuperType.call(this, name);
}
inheritPrototype(SubType, SuperType);
var sub = new SubType('john');

这段代码采用了寄生组合式继承,它和组合继承唯一的区别就在于省去了一次SuperType构造函数。只有在实例化子类的时候才会调用SuperType构造函数。而inheritPrototype方法实际上是让子类的原型指向了一个实例(这个实例并不是父类的实例,而是一个原型指向父类原型的实例),也就是说该实例并没有父类中的属性,它仅仅拥有父类原型中定义的方法。所以,该方法是最好的引用类型的继承模式。记住它的名字,寄生组合式继承。为什么要叫寄生?因为在inheritPrototype函数中,创建了一个类似于父类的实例,只是该实例没有父类的属性罢了。其实吧,我个人觉得可以把inheritPrototype改写一下:

1
2
3
function inheritPrototype(subType, superType){
subType.prototype = superType.prototype;
}

将子类的原型直接指向父类的原型。这个效果和前面的效果是一样的,不知道为什么要创建一个中间实例。作为桥梁,连接子类的原型和父类的原型。
最后还有两种继承模式我就不提了,缺点和原型链模式以及借用构造函数模式一样。

二、闭包

闭包是js中一种特殊的存在,是js的一个特色。究竟怎么去理解闭包,闭包有什么作用,是我们这种新手常常疑惑的地方。首先来讲讲

什么是闭包?

书上的定义是,有权访问另一个函数作用域中的变量的函数。我们首先来想一个问题,我们怎么访问某一个函数作用域中定义的变量?

1
2
3
4
5
6
function F(){
var a = 1;
return function(){
return a;
}
}

看一下上面这段代码,我们知道函数作用域F中定义的变量a,因为作用域链的关系,函数F外部的环境是无法访问的到a的。因而如果我们想要访问函数F内部定义的变量a,我们可以在函数F内部定义一个匿名函数,而这个匿名函数是有权访问函数F内部的变量a的。我们把定义的这个匿名函数叫做内部匿名函数,而函数F叫做包含函数(或外部函数)。有一个问题,为什么包含函数外部不可以访问变量a,而内部匿名函数却可以。刚才说过,这是因为作用域链的关系,但为什么作用域链可以呢?

函数作用域链

当一个函数在执行的时候,后端会创建一个执行环境,而每一个执行环境都有一个表示变量的对象,叫变量对象。并且会创建一个作用域链,位于作用域链顶端的是当前函数的变量对象,接着沿着作用域链,依次是函数的外部函数的变量对象,外部函数的外部函数的变量对象,直至全局作用域的变量对象。而如果我们要查找某一个变量或函数,会沿着当前的作用域链,一级一级地往外找,直到全局作用域,如果找不到会报错。所以,这就是为什么在正常情况下,在函数的外部无法访问函数内部定义的变量的原因,函数的外部只能访问函数外部及以外定义的变量。
有了函数作用域链,再将内部匿名函数返回,就构成了一个标准的闭包。有了闭包我们可以做两件事情:

(1)模仿块级作用域

什么是块级作用域?看一下下面这段代码:

1
2
3
4
5
6
function outputNumber(num){
for(var i = 0; i < num; i++){
alert(i);
}
alert(i); //i等于num
}

javascript和其它语言的不同之处就在于,js里是没有块级作用域的概念的。在最后一个alert输出的时候,i等于num。就是因为缺少块级作用域,for循环之后的i,没有被垃圾回收,内存里依然保持着值。但是有了闭包模仿的块级作用域就不一样了。

1
2
3
4
5
6
7
8
9
10
function outputNumber(num){
(function(){
//这里是块级作用域开始
for(var i = 0; i < num; i++){
alert(i);
}
//这里是块级作用域结束
})();
alert(i); //报错
}

这段代码执行之后,会导致一个错误,因为i已经被垃圾回收了。这就是闭包模仿块级作用域的好处。有一个需要特别注意的地方,千万不要写成:

1
2
3
function(){
...
}()

这段代码会导致错误,因为js在解析function的时候,会把它当成函数声明,但是如果加了括号之后,就会当成函数表达式来对待。

(2)访问私有变量

这个概念在一开始的闭包说明中,已经提到过。来看一下下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyObject(){
//私有变量和函数
var privateVar = 1;
function privateFunc(){
return false;
}
//特权方法
this.publicMethod = function(){
privateVar++;
return privateFunc();
}
}
var myObject = new MyObject();
myObject.publicMethod(); //false

以上这段代码告诉我们,函数内部的私有变量和函数是可以被外部函数访问的,具体方法就是要在函数内部定义一个闭包并将其返回或者将其赋给函数内部的属性值。而可以访问函数内部私有变量的方法叫做特权方法。严格来说,privateFunc也是闭包,只是我们将那些匿名函数作为返回值的,或者将匿名函数给予函数某个属性的那种闭包,才是我们关注的闭包。因为这种闭包会对包含函数的变量对象保有引用。当作用域链被销毁的时候,函数的变量对象却还是保存在内存中。大量的使用这种闭包,会导致内存消耗过高。所以,那些我们不需要的闭包,就手动对其进行解引。

avatar

chilihotpot

You Are The JavaScript In My HTML