1.上下文和作用域的概念
首先,上下文和作用域是两个不同的概念,多年来很多开发者会混淆这两个概念(我自己也是),函数调用和作用域和上下文紧密相关,作用域是对于函数而言的(除了全局作用域),只有函数才会创建作用域,函数定义的时候作用域就确定好了,无论你调用不调用,你只要创建了函数,它就会有个单独作用域,一个属于自己的地盘,而上下文是对于对象而言的,简单的说作用域涉及到被调用的函数中的变量的访问,上下文始终是this的值(和执行上下文不是一个概念,我下面会详细讲解),这个是由函数运行时决定的,简单来说就是谁调用此函数,this就指向谁,就有一个这个函数的上下文,一个作用域下可能会有多个上下文(函数被调用多次),也可能没有(函数未调用过),当函数调用完成,上下文环境将被摧毁。看下面的例子:
var x = 100;
//全局作用域
function fn(x) {
//fn 作用域
console.log(x,this);//5,window
var obj = {
fn1:function(){
// fn1作用域
var n=10;
console.log(n,this==obj) //10,true
}
}
obj.fn1()
};
fn(5);
这里有3个作用域,一个全局作用域,fn作用域,fn1作用域,刚一开始全局作用域中有x、fn函数,当执行fn(5)时候,会创建一个上下文执行环境,此时的上下文是window, 此时fn作用域中包括x=5, obj对象,然后执行obj.fn1();又创建了一个上下文,因为调用者是obj,所以这个上下文环境是obj对象,此环境中 fn1作用域包括 n=10, 所以全局作用域——fn作用域——fn1作用域就形成了一个作用域链式。(可能这个例子不太具体)。我稍微改下例子:
var x = 100;
//全局作用域
function fn(x) {
//fn 作用域
console.log(x,this);//5,window
var n=10;
function fn1(){
// fn1作用域
console.log(n,this)//10,window
}
fn1()
};
fn(5);
fn(10)
调用2次fn,所以在fn作用域中产生了2次上下文,因为函数自调用所以this都是指向window。作用域的访问原则,子可以访问父及作用域的变量,父级不能访问子的作用域的变量,这是个不可逆的访问,当调用fn1时候,此时fn1 作用域下没有n,所以它会向上级访问到n=10;
2.执行上下文
执行上下文是一个环境,有全局执行环境和函数执行环境两种,都包括(变量对象 /活动对象,作用域链,this的值),所有一个执行上下文环境受多方面影响,如果函数被调用,创建了一个执行上下文环境,首先会创建好一个执行上下文,因为只有先创建好了才会被压入栈中。
创建阶段:生成变量对象(Variable object, VO),建立作用域链(Scope chain),确定this指向。
执行阶段:变量赋值,函数引用,执行其他代码。
创建阶段
我们可以把上下文环境看成一个对象
exeContent = {
VO =[], //代表变量对象,保存变量和函数申明
scopeChain =[],//作用域链
thisValue ={},//this的值
}
创建上下文就是创建活动对象,创建作用域链和this的过程。下面介绍下这三个东西到底是啥玩意?
变量对象
变量对象储存着上下环境定义的变量和函数声明,其实这是一个变量和函数声明提升的一个过程。
console.log(a);//undefined;
console.log(b);//b is not defined
console.log(c);//function c(){};
console.log(d);//undefined
var a = 100;
b = 10;
function c(){};
var d = function(){};
上面代码进行变量和函数声明提升
function c(){};
var a
var d
那么创建过程中变量对象是
VO = {
a = undefined; //有a,a使用var声明,值会被赋值为undefined
//没有b,因为b没用var声明
c = function c (){} //有c,c是函数声明,并且c指向该函数
d = undefined; //有d,d用var声明,值会被赋值为undefined
}
执行上述代码的时候,会创建一个全局执行上下文,上下文中包含上面变量对象,创建完执行上下文后,这个执行上下文才会被压进执行栈中。开始执行后,因为js代码一步一步被执行,后面赋值的代码还没被执行到,所以使用console.log函数打印各个变量的值是变量对象中的值。在往下执行阶段了。
a=100;
b=10;
d=function(){};
VO = {
a = 10 ,
b=10,
c = function c (){},
d = function d(){}
}
活动对象
当一个函数被调用时,一个特殊的对象活动对象就会被创建,这个对象中包含形参和那个特殊的arguments对象,只是它需要在函数被调用时才被激活,激活之后就和变量对象一样,活动对象之后会做为函数上下文的变量对象来使用。
function fn(a, b) {
var c = 30;
function fn1() {}
}
foo(10, 20);
当上面的函数fn被调用,就会创建一个执行上下文,同时活动对象被激活。
VO = {
argument:{0:10,1:20},
a:10,
b:20,
c:undefined,//创建阶段还是undefined的,执行阶段才会赋值为30
fn1:function fn1(){}
}
活动对象其实也是变量对象,做着同样的工作。其实不管变量还是活动对象,这里都表明了,全局执行和函数执行时都有一个变量对象来储存着该上下文(环境内)定义的变量和函数。
最后说下是在javascript中上下文是怎么执行?
当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行栈顶部(作用域链)。浏览器总是执行位于执行栈顶部的当前执行上下文,一旦执行完毕,该执行上下文就会从执行栈顶部弹出,并且控制权将进入其下的执行上下文。这样,执行栈中的执行上下文就会被依次执行并且弹出,直到回到全局的执行上下文。
3.作用域(ES5),作用域链(scope chain)
1.全局变量:声明在函数外部的变量,在代码中任何地方都能访问到的对象拥有全局作用域(所有没有var直接赋值的变量都属于全局变量)。
2.局部变量:声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域(所有没有var直接赋值的变量都属于全局变量)。
3.在创建执行上下文时还要创建一个重要的东西,就是作用域链。每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。
4.作用域链正是内部上下文所有变量对象(包括父变量对象)的列表,作用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。
5.函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的[[scope]]属性。下面我们将更详细的讨论一个函数的[[scope]]属性。
activeExecutionContext = {
VO: {...}, // or AO
this: thisValue,
Scope: [ // Scope chain
// 所有变量对象的列表
// for identifiers lookup
]
};
下面是scope的定义:
Scope = AO + [[Scope]]
下面我们来看个例子:
var a = 10;
function fn() {
var b = 20;
alert(a + b);// 30
}
fn();
这里,我们看到变量“b在函数“fn中定义(意味着它在fn下文的AO中),但是变量“a并未在“fn"上下文中定义,相应地,它也不会添加到“fn"的AO中,变量"a"相对"fn"函数更本就不存在,"fn"上下文活动对象只有一个y属性。
fnContext.AO = {
b: undefined // undefined – 进入上下文的时候是20 – at activation
};
函数"fn"是如何能访问到变量a的,其实是通过函数内部的[[scope]]属性来实现的。注意这重要的一点[[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。这里所说的就是词法作用域下面会有实例。
函数"fn"的[[scope]]如下:
fn.[[Scope]] = [
globalContext.VO // === Global
];
正如在定义中说到的,进入上下文创建AO/VO之后,上下文的Scope属性(变量查找的一个作用域链)作如下定义:
Scope = AO|VO + [[Scope]],活动对象是作用域数组的第一个对象,即添加到作用域的前端。还是那上面那代码做例子:
全局上下文的变量对象:
globalContext.VO === Global = {
a:10
fn: <reference to function>
};
在“fn”创建时,“fn”的[[scope]]属性是:
fn.[[Scope]] = [
globalContext.VO
];
在“fn”激活时(进入上下文),“fn”上下文的活动对象是:
fnContext.AO = {
b:20
};
“fn”上下文的作用域链为:
fnContext.Scope = fnContext.AO + fn.[[Scope]] // i.e.:
fnContext.Scope = [
fnContext.AO,//活动对象是作用域数组的第一个对象
globalContext.VO
];
对"a","b"的标识符解析如下:
"a"
fnContext.AO // not found
globalContext.VO // found - 10
"b"
fnContext.AO // found - 20
了解了[[scope]]属性,就应该知道作用域链是怎么形成的,下面具体说下有哪些作用域。
1.最外层函数和在最外层函数外面定义的变量拥有全局作用域
var a = 10;
function fn() {
console.log(a);
};
fn(); //全局变量,所以可以直接访问到 结果为10
function fn() {
var a = 10;
};
fn();
consoele.log(a); //a 为函数fn()内部变量量即局部变量,所以无法访问到
2.所有末定义直接赋值的变量自动声明为拥有全局作用域
function fn() {
a = 10;
}
fn();
console.log(a); //结果为10;
//等价于:
var a;
function fn() {
a = 10;
};
fn();
console.log(a);
function fn() {
var a = b = 10;
}
fn();
console.log(a);
console.log(b);
//等价于
function fn() {
b = 10; //b为全局变量
var a = b;//a为fn函数的局部变量
}
fn();
console.log(a); //结果为,无法访问到
console.log(b); //结果为10;
3.变量的查找是就近原则去寻找,变量的声明会被提前到作用域顶部
var a = 10;
function fn() {
alert(a);
var a = 20;
}
fn();
//等价于
var a = 10;
function fn() {
var a; //声明提前了
alert(a);//结果为:undefined
a = 20; //这里才赋值
}
fn();
var a = 10;
function fn(b) {
alert(b);
var b = 20;
}
fn(a);
//等价于
var a = 10;
function fn(b) {
var b =a; // 形参b是a的值 这样默认会var b = a =10;
alert(b);
var a = 20;
}
fn(a); //结果为:10
4.js词法作用域
词法作用域就是定义此法阶段的作用域,即你写代码时将变量和作用域写在哪里而决定其作用范围,无论函数在哪里被调用,以何种方式调用,其词法作用域都只由被声明时所处的位置决定,即你写下哪他就在哪发挥作用。
var a = 10;
function fn() {
console.log(a);// a的值为全局变量10
};
function fn1() {
var a = 20;
fn();//10
}
fn1(); //当创建这个函数时候作用域就已经定义好了 所以fn()不会因为位置变化而改变它的值
4.this的值
1.当函数作为对象的方法被调用时,this 指向调用方法的对象。
var obj={
fn:function(){
console.log(this)//obj
}
}
obj.fn();
2.当调用一个函数时,通过 new 操作符创建一个对象的实例,当以这种方式调用时,this 指向新创建的实例,当调用一个未绑定函数,this 默认指向全局上下文或者浏览器中的window对象。
function Constructor(){
console.log(this);
}
Constructor();//window
var p =new Constructor();// p
3.改变上下文环境(call,apply)
var number = 100;
function Fn1() {
this.number = 10 ;
}
function Fn2() {
console.log(this.number);
}
Fn2();//this 指向window 100
Fn2.call(window); // 把this指向window 等价于 window.number 100
Fn2.call(new Fn1());// 把this指向Fn1的实例 等价于输出 (new Fn1()).number 10
Fn2.apply(window); // 把this指向window 等价于 window.number 100
Fn2.apply(new Fn1());// 把this指向Fn1的实例 等价于输出 (new Fn1()).number 10
补充一下:
在《JavaScript权威指南》中提及到:
1.this是关键字,不是变量,不是属性名,js语法不允许给this赋值。
2.关键字this没有作用域限制,嵌套的函数不会从调用它的函数中继承this。
3.如果嵌套函数作为方法调用,其this指向调用它的对象。
4.如果嵌套函数作为函数调用,其this值是window(非严格模式),或undefined(严格模式下)。