第一节


“一切都是对象”这句话的重点在于如何去理解”对象”这个概念。
在js中也不是所有的都是对象,值类型就不是对象。
首先我们还是先看看javascript中一个常用的函数—typeof()。typeof应该算是我们的老朋友,还有谁没有用过它?
typeof函数输出的一共有几种类型,在此列出:
1
2
3
4
5
6
7
8
9
10
11
12
function show(x){
console.log(typeof(x)); //undefined
console.log(typeof(10));//number
console.log(typeof('abc'));//string
console.log(typeof(true));//boolean
console.log(typeof(function(){})); //function
console.log(typeof([1,'a',true]));//object
console.log(typeof({a:10,b:20}));//object
console.log(typeof(null));//object
console.log(typeof(new Number(10))); //object
}
show();

以上代码列出了typeof输出的集中类型标识,其中上面的四种(undefined,number,string,boolean)属于简单的值类型,不是对象。剩下的几种情况—函数、数组、对象、null、new Number(10)都是对象,都是引用类型。
判断一个变量是不是对象非常简单。值类型判断用typeof,引用类型的类型判断用instanceof。

1
2
var fn = function(){};
console.log(fn instanceof Object); //true

对象—若干属性的集合


java或c#中的对象都是new一个class出来的,而且里面有字段、属性、方法、规定的非常严格。但是js就比较随意了—数组是对象,函数是对象,对象还是对象。对象里面的一切都是属性,只有属性,没有方法。那么这样方法如果表示呢?
–方法也是一种属性。因为它的属性表示为键值对的形式。
而且,更加好玩的是,js中的对象可以任意的扩展属性,没有class的约束。
先说个最常用的例子:
1
2
3
4
5
6
7
8
9
10
var obj = {
a:10,
b:function(x){
alert(this.a + x);
},
c:{
name:'杨灿',
year:1990
}
};

以上代码中,obj是一个自定义的对象,其中a,b,c就是它的属性,而且在c的属性值还是一个对象,它又有name,year两个属性。
这样可能比较好理解,那么函数和数组也可以这样定义属性吗?—当然不行,但是它可以用另一种形式,总之函数/数组之流,只要是对象,它就是属性的集合。
以函数为例子:

1
2
3
4
5
6
7
8
9
10
11
var fn = function(){
alert(100);
};
fn.a = 10;
fn.b = function(){
alert(123);
};
fn.c = {
name:'杨灿',
year:1990
}

以上代码中,函数就作为对象被赋值了a,b,c三个属性—很明显,这就是属性的集合吗。
在jQuery源码中,”jQuery”或者”$”,这个变量其实是一个函数。

1
console.log(typeof($));//function

验明正身!的确是个函数。那么我们常用的$.trim()也是个函数。
很明显,这就是在$或者jQuery函数上加了一个trim属性,属性值是函数,作用就是截取前后空格。
最后有个疑问。在typeof的输出类型中,function和object都是对象,为何却要输出两种答案?都叫做object不行吗?—当然不行。
具体原因,且听下回分解!

第二节


上文已经提到,函数就是对象的一种,因为通过instanceof函数可以判断。
1
2
var fn = function(){};
console.log(fn instanceof Ojbect);//true

对!函数是一种对象,但是函数却不像数组一样—你可以说数组是对象的一个子集一样。但是函数与对象之间,却不仅仅是一种包含和被包含的关系。
先看一个小例子吧。

1
2
3
4
5
function Fn(){
this.name = '养蚕';
this.year = 1990;
}
var fn = new Fn();

上面的这个例子很简单,它能说明:对象可以通过函数来创建。对!也只能说明这一点。
但是我要说—对象都是通过函数创建的—有些人可能反驳:不对!因为:

1
2
var obj = {a:10,b:20};
var arr = [5,'x',true];

但是不好意思,这个——真的——是一种——“快捷方式”,在编程语言中,一般叫做“语法糖”。
做“语法糖”做的最好的可谓是微软大哥,它把他们家C#那小子弄的不男不女从的,本想图个人见人爱,谁承想还得到处跟人解释——其实它是个男孩!
其实以上代码的本质是:

1
2
3
4
5
6
7
8
var obj = new Object();
obj.a = 10;
obj.b = 20;

var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;

而其中的Object和Array都是函数

1
2
console.log(typeof(Object));//function
console.log(typeof(Array));//function

所以可以负责任的说:

函数是对象的一种,对象是通过函数来创建的


现在是不是糊涂了—— 对象是函数创建的,而函数却又是一种对象——天哪!函数和对象到底是什么关系啊?
别着急!揭开这个谜底,还得先去了解一下另一位老朋友——prototype原型。

第三节


上一节中说道,函数也是一种对象。它也是属性的集合,也可以对函数进行自定义属性。
不用等我们去实验,js自己就先做了表率,人家就默认的给函数一个属性—prototype。每一个函数都有一个属性叫做prototype。
这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor属性,指向这个函数本身。
原型既然作为对象,属性的集合,不可能就只弄个constructor来玩玩,肯定可以自定义的增加许多属性,例如这位Object大哥,人家的prototype里面,就有好几个其他属性。
1
2
3
4
5
6
7
8
Object           Object prototype
prototype constructor
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

你也可以自己自定义的方法的prototype中新增自己的属性。

1
2
3
4
5
function Fn(){}
Fn.prototype.name='yc';
Fn.prototype.getYear = function(){
return 1990;
};

这样就变成
Fn Fn Prototype
prototype constructor
name ‘yc’
getYear (function)
但是,这样做有何作用呢?—-解决这个问题,我们还是先说说jQuery吧。

1
2
var $div = $('div');
$div.attr('myName','yc');

以上代码中,$(‘div’)返回的是一个对象,对象—被函数创建的。假设创建这一对象的函数是myjQuery.它其实是这样实现的。

1
2
3
4
myjQuery.prototype.attr = function(){
//....
};
$('div') = new myjQuery();

如果用咱们自己的代码来演示,就是这样

1
2
3
4
5
6
7
8
function Fn(){}
Fn.prototype.name = 'yc';
Fn.prototype.getYear = function(){
return 1990;
};
var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());

即,Fn是一个函数,fn对象是从Fn函数new出来的,这样fn对象就可以调用Fn.prototype中的属性。
因为每一个对象都有一个隐藏的属性—“proto“,这个属性引用了创建这个对象的函数的prototype。即:fn.proto === Fn.prototype

第四节


上节已经提到,每个函数function都有一个prototype,即原型。这里再加一句话—每个对象都有一个proto,可成为隐式原型。
这个proto是一个隐藏的属性,javascript不希望开发者用到这个属性值。
1
2
var obj = {};
console.log(obj.__proto__);

可以看出obj.proto和Object.prototype的属性一样!这么巧!
答案就是一样。
obj这个对象本质上是被Object函数创建的,因此obj.proto ===Object.prototype。

即,每个对象都有一个proto属性,指向创建对象的函数的prototype。
那么上面”Object prototype”也是一个对象,它的proto指向哪里?
在说明这个问题之前,先说一下自定义函数的prototype。自定义函数的prototype本质上就是和var obj={}是一样的,都是被Object创建,所以它的proto指向的就是Object.prototype。
但是Object.prototype确实一个特例—-它的proto指向的是null,切记切记!

还有—函数也是一种对象,函数也有proto吗?
函数也是被创建出来的。谁创建了函数呢?—Function—注意这个大写的F。
且看如下代码.

1
2
3
4
5
6
function fn(x,y){
return x + y;
};
console.log(fn(10,20));
var fn1 = new Function("x","y","return x+y");
console.log(fn1(5,6));

以上代码中,第一种方式是比较传统的函数创建方式,第二种是用new Function创建。
首先根本不推荐用第二种方式。这里只是向大家演示,函数是被Function创建的。
根据对象的proto指向的是创建它的函数的prototype,就会出现:Object.proto===Function.prototype。用一个图来表示。

上图中,很明显的标出了:自定义函数Foo.proto指向Function.prototype,Object._proto指向Function.prototype,唉,怎么还有一个……Function.proto指向Function.prototype?这不成了循环引用了?
对!是一个环形结构。
其实稍微想一下就明白了。Function也是一个函数,函数是一种对象,也有
proto属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的。所以它的proto指向了自身的Prototype。
最后一个问题:Function.prototype指向的对象,它的
proto__是不是也指向Object.prototype?

答案是肯定的。因为Function.prototype指向的对象也是一个普通的被Object创建的对象,所以也遵循基本的规则。

第五节


又介绍一个老朋友—instanceof
对于值类型,你可以通过typeof判断,string/number/boolean都很清楚,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组,还是new Number等等。
这个时候就需要用到instanceof。例如:
1
2
3
4
function Foo(){}
var f1 = new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object);//true

上面的代码中,f1这个对象是被Foo创建,但是”f1 instanceof Object”为什么是true呢?
至于为什么先看下图:

Instanceof运算符的第一个变量是一个对象,叫做A;第二个变量一般是一个函数,叫做B。
Instanceof的判断规则是:沿着A的proto这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么久返回true。如果找到终点还未重合,则返回false。
按照以上规则,可以看出”f1 instanceof Object” 这句代码是不是true?根据上图很容易就能看出来,就是true。
通过以上规则,你可以解释很多怪异的现象,例如:

1
2
3
console.log(Object instanceof Function);//true
console.log(Function instanceof Object);//true
console.log(Function instanceof Function);//true

这些看视很混乱的东西,答案却都是true,为什么呢?》
看下图:

看这个图片,千万不要嫌烦,必须一条线一条线挨着分析。如果上一节你看的比较仔细,再结合刚才咱们介绍的instanceof的概念,相信能看懂这个图片的内容。

看看这个图片,你也就知道为何上面三个看似混乱的语句返回的是true了。

问题又出来了。Instanceof这样设计,到底有什么用?到底instanceof想表达什么呢?

重点就这样被这位老朋友给引出来了——继承——原型链。

即,instanceof表示的就是一种继承关系,或者原型链的结构。请看下节分解。

第六节


原型链如果解释清楚了很容易理解,不会与常用的java/c#产生混淆。而”继承”确实常用面向对象语言中最基本的概念,但是java中的继承与javascript中的继承又完全是两回事儿。
javascript中的继承是通过原型链来体现的。先看几句代码
1
2
3
4
5
6
7
function Foo(){}
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a);//10
console.log(f1.b);//200

以上代码中,f1是Foo函数new出来的对象,f1.a是f1对象的基本属性,f1.b是从Foo.prototype得来的。因为f1.proto指向的是Foo.prototype
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着proto者条链上找,这就是原型链。
看图说话:

上图中,访问f1.b时,f1的基本属性中没有b,于是沿着proto找到了Foo.prototype。
那么我们在实际应用中如何区分一个属性到底是基本的还是从原型中找到的呢?大家可能都知道了答案—hasOwnProperty,特别是在for…in…循环中,一定要注意。

f1的这个hasOwnProperty方法是从哪里来的?f1本身没有,Foo.prototype中也没有。其实它是从Object.prototype中来的。看下图:

对象的原型链是沿着proto这条线走的,因此在查找f1.hasOwnProperty属性时,就会顺着原型链一直查找到Object.prototype。
由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法,者就是所谓的”继承”。
当然这只是一个例子,你可以自定义函数和对象来实现自己的继承。
说一个函数的例子吧。
我们都知道每个函数都有call,apply方法,都有length,arguments,caller等属性。为什么每个函数都有呢?这肯定是”继承”的。函数由Function函数创建,因此继承的Function.prototype中的方法。看下图:

看到了吧,有call,length等这些属性。
那怎么还有hasOwnProperty呢?—那是Function.prototype继承自Object.prototype的方法。有疑问可以看看上一节的instanceof。看看Function.prototype.proto是否指向Object.prototype。

第七节


在java和c#中,你可以简单的理解class是一个模子,对象就是被这个模子压出来的一批一批月饼。压个啥样,就得是个啥样,不能随便动,动一动就坏了。
而在javascript中,就没有模子了,月饼换成了面团,你可以捏成自己想要的样子。
首先,对象属性可以随时改动。
对象或者函数,刚开始new出来之后,可能啥属性都没有,但是你可以这会加一个,过一会儿在加两个,非常灵活。
在jQuery的源码中,对象被创建时什么属性都没有,都是代码一步一步执行是,一个一个加上的。

其次,如果继承的方法不合适,可以做出修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {a:10,b:20};
console.log(obj.toString());//[object Object]

var arr = [1,2,true];
console.log(arr.toString());//1,2,true
```
如上图,Object和Array的toString()方法不一样。肯定是Array.prototype.toString()方法做了修改。
同理,我也可以自定义一个函数,并自己去修改prototype.toString()方法。
``` bash
function Foo(){}
var f1 = new Foo();
Foo.prototype.toString = function (){
return '中国';
}
console.log(f1.toString());//中国

最后,如果感觉当前缺少你要用的方法,可以自己去创建。
例如在json2.js源码中,为Date,String,Number,Boolean方法添加一个toJSON的属性。

如果你要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加,如果本来就存在,就没必要再添加了。

第八节


什么是”执行上下文”?先看一段代码

第一句报错,a未定义,很正常。第二句,第三局输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三局中)。
在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些”准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可以用下图来模拟:

下面还有。先来个简单的。

有js开发经验的朋友应该都知道,你无论在哪个位置获取this,都是有值的。至于this的取值情况,比较复杂,会专门拿出一篇文章来讲解。
与第一种情况不同的是:第一种情况只是对变量进行了声明(并没有赋值),而此种情况直接给this赋值。这也是”准备工作”情况要做的事情之一。
第三种情况:就是”函数表达式”和”函数声明”。虽然两者很常用,但是这两者在”准备工作”时,却是两种待遇。
1
2
3
4
console.log(f1);//function f1(){}
function f1(){} //函数声明
console.log(f2); //undefined
var f2 = function(){};//函数表达式

看以上代码。”函数声明”时我们看到了第二种情况的影子,而”函数表达式”时我们看到了第一种情况的影子。
没错。在”准备工作”中,对待函数表达式就像对待”var a = 10” 这样的变量一样,只是声明。而对待函数声明是,却把函数整个赋值了。
好了,”准备工作”介绍完毕。
我们总结一下,在”准备工作”中完成了哪些工作:
1,变量、函数表达式—变量声明,默认赋值为undefined;
2,this—-赋值;
2,函数声明—赋值;
这三种数据的准备情况我们叫做”执行上下文”或者”执行上下文环境”。
细心的朋友可能会发现,我们上面所有的例子都是在全局环境下执行的。
其实,javascript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况——全局代码,函数体,eval代码。
这里解释一下为什么代码段分为这三种。

所谓“代码段”就是一段文本形式的代码。

首先,全局代码是一种,这个应该没有非议,本来就是手写文本到
其次,eval代码接收的也是一段文本形式的代码。
eval(“alert(123)”);
最后,函数体是代码段是因为函数在创建时,本质上是 new Function(…) 得来的,其中需要传入一个文本形式的参数作为函数体。

这样解释应该能理解了。
最后,eval不常用,也不推荐大家用。

第九节


上一节我们讲到了在全局环境下的代码段中,执行上下文环境中的”准备工作”
1,变量、函数表达式—-变量声明,默认赋值为undefined;
2,this—赋值;
3,函数声明—赋值;
如果在函数中,除了以上数据之外,还会有其他数据。先看以下代码:
1
2
3
4
5
function fn(){
console.log(arguments); //[10]
console.log(x); //10
}
fn(10);

以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
另外一点不同在于,函数在定义的时候(不是在调用的时候),就已经确定了函数体内部自由变量的作用域。如下:

1
2
3
4
5
6
7
8
9
var a = 10;
function fn(){
console.log(a); //a是自由变量,函数创建时,就确定了a要取值的作用域。
}
fucntion bar(f){
var a = 20;
f();//打印"10",而不是"20"
}
bar(fn);

总结:
函数体内”准备工作”:
参数:赋值
arguments:赋值
自由变量的取值作用域:赋值
给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
讲完了上下文环境,又来了新的问题——在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?下一节将通过“执行上下文栈”来解释这个问题。

不过别着急,在解释“执行上下文栈”之前,还需要把this说一下,this还是挺重要的。

说完this,接着说执行上下文栈。

第十节


接着上一节讲的话,应该轮到”执行上下文栈”了,但是这里不得不插入一节,把this说一下,因为this很重要,js面试题如果不出几个与this有关的。那出题者都不合格了。
其实this的取值,分四种情况。
在此再强调一遍一个非常重要的知识点:在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。
情况1:
所谓构造函数就是用来new对象的函数,其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数第一个字母大写,例如:Object,Array,Function等。
1
2
3
4
5
6
7
8
function Foo(){
this.name = 'yc';
this.year = 1990;
console.log(this); //Foo{name:'yc',year:1990}
}
var f1 = new Foo();
console.log(f1.name);//yc
console.log(f1.year);//1990

以上代码中,如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。
注意,以上仅限new Foo()的情况,即Foo函数作为构造函数的情况。如果直接调用Foo函数,而不是new Foo(),情况就大不一样了。

1
2
3
4
5
6
function Foo(){
this.name = 'yc';
this.year = 1990;
console.log(this);//Window{top:Window,window:Window,location:Location,exter}
}
Foo();

这种情况下this是window。
情况2:函数作为对象的一个属性
如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。

1
2
3
4
5
6
7
8
var obj = {
x:10,
fn:function(){
console.log(this); //Object {x:10,fn:function}
console.log(this.x);//10
}
};
obj.fn();

以上代码中,fn不仅作为一个对象的一个属性,而且的确是作为对象的一个属性被调用。结果this就是obj对象。
注意:如果fn函数不作为obj的一个属性被调用,会是什么结果呢?

1
2
3
4
5
6
7
8
9
var obj = {
x:10,
fn:function(){
console.log(this); //Window
console.log(this.x);//undefined
}
};
var fn1 = obj.fn;
fn1();

以上代码,如果fn函数被赋值到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,this.x为undefined。
情况3:
当一个函数被call和apply调用时,this的值就取传入的对象的值。代码如下:

1
2
3
4
5
6
7
8
var obj = {
x:10
};
var fn = function(){
console.log(this); //Object{x:10}
console.log(this.x); //10
}
fn.call(obj);

情况4:
在全局环境下,this永远是window,这个应该没有非议。

1
console.log(this === window);   //true

普通函数在调用时,其中的this也都是window。

1
2
3
4
5
6
var x = 10;
var fn = function(){
console.log(this); //Window{top:Window}
console.log(this.x);//10
}
fn();

以上代码很好理解。
不过下面的情况需要注意一下:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
x:10,
fn:function(){
function f(){
console.log(this);// Window
console.log(this.x);//undefined
}
f();
}
};
obj.fn();

函数f虽然是在obj.fn内部定义的,但是它任然是一个普通的函数,this任然指向window。

第十一节


继续上文的内容
执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被清除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。
其实这是一个压栈出栈的过程—-执行上下文栈。如下图:

可根据以下代码来详细介绍上下文栈的压栈、出栈过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
1 var a = 10,         //1,进入全局上下文环境
2 fn,
3 bar = function(){
4 var b = 5;
5 fn(x + b); //3,进入fn函数上下文环境
6 };
7
8 fn = function(y){
9 var c = 5;
10 console.log( y+c );
11 }
12
13 var(10); //2,进入bar函数上下文环境

在执行代码之前,首先将创建全局上下文环境。

然后是代码执行。代码执行到bar(10)之前,上下文环境中的变量都在执行过程中被赋值。

执行到bar(10),调用bar函数。
跳转到bar函数内部,执行函数体语句之前,会创建一个新的执行上下文环境。

并将这个执行上下文压栈,设置为活动状态。

执行到第5行,又调用了fn函数。进入fn函数,在执行函数体语句之前,会创建fn函数的执行上下文环境,并压栈,设置为活动状态。

待第5行执行完毕,即fn函数执行完毕后,此次调用fn所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。

同理,待第13行执行完毕,即bar函数执行完毕后,调用bar函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)

好了,我很耐心的给大家介绍了一段简短代码的执行上下文环境的变化过程,一个完整的闭环。其中上下文环境的变量赋值过程我省略了许多,因为那些并不难,一看就知道。

讲到这里,我不得不很遗憾的跟大家说:其实以上我们所演示的是一种比较理想的情况。有一种情况,而且是很常用的一种情况,无法做到这样干净利落的说销毁就销毁。这种情况就是伟大的——闭包。

要说闭包,咱们还得先从自由变量和作用域说起。

第十二节


提到作用域,有一句话可能比较熟悉:”javascript没有块级作用域”。所谓”块”,就是大括号”{}”中间的语句,例如if语句。
1
2
3
4
5
var i = 10;
if(i > 1){
var name = 'yc';
}
console.log(name);//yc

再比如for语句:

1
2
3
4
for(var i = 0;i < 10 ; i++){
//....
}
console.log(i); //10;

所以,我们在编写代码的时候,不要在”块”里面声明变量,要在代码的一开始就声明好了。以避免发生歧义。如:

1
2
3
4
5
var i;
for(i = 0;i < 10;i++){
//...
}
console.log(i);//10

其实,你光知道javascript没有块级作用域是完全不够的,你需要知道的是–javascript除了全局作用域之外,只有函数可以创建作用域。
所以,我们在声明变量时,全局代码要在代码前端声明,函数中药在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用”单var”形式。
下面继续来说作用域,作用域是一个很抽象的概念,类似一个”地盘”

如上图,全局代码合fn,bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪里个作用域下创建的。例如,fn作用域下创建了bar函数,那么”fn作用域”就是”bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。例如以上代码中,三个作用域下都声明了”a”这个变量,但是他们不会有冲突。各自的作用域下,用各自的”a”。

第十三节


上文简单介绍了作用域,本文把作用域和上下文环境结合起来说一下,会理解的更深一些。

如上图,我们在上下中已经介绍了,除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。
第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量进行赋值。

第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。

第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。

第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。

第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变成活动状态。

第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。

结束了。像老太太的裹脚布——又臭又长!
最后我们可以把以上这几个图片连接起来看看。

总结:作用域只是一个”底盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量
的值是在执行过程中产生的确定的,而作用域却是在函数创建时确定了。
所以,如果要查找一个作用域下某一个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

第十四节


先解释一下什么是”自由变量”。
在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。如下图:
1
2
3
4
5
var x = 10;
function fn(){
var b = 20;
console.log(x + b);//这里的x在这里就是一个自由变量
}

如上代码中,在调用fn()函数时,函数体中取b的值就直接可与在fn作用域中取,因为b就是在这里定义的。而取x的值时,就需要到另外一个作用域中取。到哪个作用域中取呢?
有人说过要到父作用域中取,其实有时候这种解释会产生歧义。例如:

1
2
3
4
5
6
7
8
9
10
11
var x = 10;
function fn(){
console.log(x);
}
function show(f){
var x = 20;
(function(){
f();
})();
}
show(fn);

所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切—-要到创建这个函数的那个作用域中取值— 是”创建”,而不是”调用”,切记切记—其实这就是所谓的”静态作用域”。
对于本文第一段代码,在fn函数中,取自由变量x的值时,要到哪个作用域中取?—要到创建fn函数的那个作用域中取—物论fn函数将在哪里调用。
上面描述的只是跨一步作用域去寻找。

如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

这个一步一步“跨”的路线,我们称之为——作用域链。

我们拿文字总结一下取自由变量时的这个“作用域链”过程:(假设a是自由量)

第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

第四步,跳转到第一步

以上代码中:第13行,fn()返回的是bar函数,赋值给x。执行x(),即执行bar函数代码。取b的值时,直接在fn作用域取出。取a的值时,试图在fn作用域取,但是取不到,只能转向创建fn的那个作用域中去查找,结果找到了。
这一节看似很轻松的把作用域链引出来,并讲完了。之所有轻松是有前几节的基础,否则将很难解释。

接下来咱们开始正式说说一直期待依旧的朋友——闭包。敬请期待下一节。

第十五节


前面提到的上下文环境和作用域的知识,除了了解这些知识之外,还是理解闭包的基础。
闭包你只需要知道应用的两种情况即可—函数作为返回值,函数作为参数传递。
第一,函数作为返回值
1
2
3
4
5
6
7
8
9
10
11
function fn(){
var max = 10;

return function bar(x){
if(x > max){
console.log(x);
}
};
}
var f1 = fn();
fl(15);

如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。至于如何跨作用域取值,可以参考上一节。
第二,函数作为参数被传递

1
2
3
4
5
6
7
8
9
10
var max = 10,
fn = function(x){
if( x > max){
console.log(x);
}
};
(function(f){
var max = 100;
f(15);
})(fn);

以上代码中,fn函数作为一个参数被传递进入另外一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。
上一节讲到自由变量跨作用域取值时,曾经强调过:要去创建这个函数的作用域取值,而不是”父作用域”。理解了这一点,以上两端代码中,自由变量如果取值应该比较简单。
另外,讲到闭包,除了结合着作用域之外,还需要结合着执行上下文栈来说一下。
前面讲到执行上下文栈时,我们提到当一个函数被调用完成之后,起执行上下文环境将被销毁,其中的变量也会被同时销毁。
但是在当时那篇文章中留了一个问号—有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。
咱们可以拿本文的第一段代码(稍作修改)来分析一下。

第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。

第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。

第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。
因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。
——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图:

第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。

这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。

使用闭包会增加内容开销,现在很明显了吧!

第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。

闭包和作用域、上下文环境有着密不可分的关系,真的是“想说爱你不容易”!

另外,闭包在jQuery中的应用非常多,在这里就不一一举例子了。所以,无论你是想了解一个经典的框架/类库,还是想自己开发一个插件或者类库,像闭包、原型这些基本的理论,是一定要知道的。否则,到时候出了BUG你都不知道为什么,因为这些BUG可能完全在你的知识范围之外
本文对《深入理解javascript原型和闭包(10)——this》一篇进行补充,原文链接:http://www.cnblogs.com/wangfupeng1988/p/3988422.html

原文中,讲解了在javascript中this的各个情况,写完之后发现还落下一种情况,就此补充。

原文中this的其中一种情况是构造函数的,具体的内容可以参考原文,此处不再赘述。

要补充的内容是,在构造函数的prototype中,this代表着什么。

如上代码,在Fn.prototype.getName函数中,this指向的是f1对象。因此可以通过this.name获取f1.name的值。

其实,不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。