JavaScript作用域

Scope

作用域的概念

各个变量、函数和对象的可访问性。作用域决定了代码里的变量和其他资源在各个区域中的可见性。

JavaScript中的作用域

JavaScript的作用域分为:

  • 全局作用域
  • 局部作用域

当变量定义在一个函数中时,变量保存在局部作用域中。定义在函数之外的变量则属于全局作用域。每个函数在调用时都会创建自己的作用域。

全局作用域

在文档中(document)中编写JS时,此时申明的变量和函数都在全局作用域中。

1
var name = "Nancy";

全局作用域下的变量能够在其他作用域中被访问和修改。

局部作用域

定义在函数中的变量保存在局部作用域中。并且函数在每次调用时都有自己的作用域。同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有各自的作用域,彼此之间不能访问。

块语句

块级声明包括ifswitch,以及forwhile循环,与函数不同的是,它们都不会创建新的作用域(ES5以前),在块级声明中定义的变量属于该块所在的作用域。

1
2
3
4
if(true){
var name = "Nancy";
}
console.log(name);//"Nancy"

ES6 引入了letconst关键字,同样作为声明,它们可以创建块级作用域。

以上的代码如果使用了这两个关键字声明,那么在块级作用域以外的作用域是不可访问到的。一个执行环境中全局作用域的生存周期与该执行环境相同。局部作用域只会在该函数调用执行期间存在。

执行上下文

执行上下文与作用域并不相同,执行上下文关系到的是函数执行时的this值。在全局作用域中,执行上下文始终是Window对象,Node环境下是global。

当程序开始的时候进入全局执行上下文,此时,全局上下文位于栈底并且是栈中的第一个元素。当在全局上下文中调用一个函数时,程序流就进入该被调用函数内,引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,继续执行新的栈顶执行上下文。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到重新回到全局的上下文。

关于执行上下文有这样几个概念:

单线程同步执行、唯一的一个全局上下文,函数上下文个数没有限制,函数被调用就会创建一个新的上下文。

执行环境

了解过JavaScript事件循环机制应该知道,JavaScript是一种单线程语言,在同一时间内只能执行单个任务。其他的任务排列在执行环境中。当解析器开始执行代码时,环境(作用域)默认设为全局。全局环境添加到执行环境中。这是执行环境的第一个环境。

每个函数在调用时都会创建自己的执行环境,并且添加它的环境到执行环境中。

当浏览器执行完环境中的代码,这个环境会从执行环境中跳出,控制权交回给之前的父环境。浏览器总是先执行在执行栈顶的执行环境(最里层的作用域)。

全局环境只有一个,函数环境可以有任意多个。
执行环境有两个阶段:创建和执行。

创建阶段

函数刚被调用但是还未执行时的阶段,主要做了三件事:

  • 创建变量对象
  • 创建作用域链
  • 设置上下文(this)的值
变量对象

变量对象(Variable Object)也称为活动对象(activation object),包含了所有变量、函数和其他在执行环境中定义的声明。当函数调用时,解析器扫描所有资源,包括函数参数、变量和其他声明。以上的所有东西装进一个对象中,它就是变量对象。

作用域链

在变量对象之后创建,它包含了自己执行环境以及所有父环境中包含的变量对象,用于解析变量,保证变量的有序访问。

执行环境对象

执行环境对象可以用下面抽象对象表示:

1
2
3
4
5
executionContextObject = {
'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
'variableObject': {}, // contains function arguments, inner variable and function declarations
'this': valueOfThis
}

代码执行阶段

执行环境的第二个阶段就是代码执行阶段,进行其他赋值操作并且代码最终被执行。

  • 变量赋值
  • 执行代码

词法作用域

当函数嵌套时,内层函数有权访问父级作用域的变量等资源。词法作用域也与静态作用域有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
function grandfather() {
var name = 'Hammad';
// likes is not accessible here
function parent() {
// name is accessible here
// likes is not accessible here
function child() {
// Innermost level of the scope chain
// name is also accessible here
var likes = 'Coding';
}
}
}

以上的这个函数中,子执行环境可以访问name属性。在不同的执行环境中同名变量优先级在执行栈由上到下增加。内层函数(执行栈顶的环境)有更高的优先级。

闭包

闭包与词法作用域相关,当内部函数试图访问外部函数的作用域链(词法作用域之外的变量)时产生闭包。闭包包括自己的作用域链、父级作用域链和全局作用域链。

闭包不仅能访问外部函数的变量,也能访问外部函数的参数。

即使闭包函数已经return,仍然能够持续的访问外部函数的所有资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
function greet() {
name = 'Hammad';
return function(){
console.log('Hi ' + name);
}
}

greet(); // nothing happens, no errors
greetLetter = greet();
// calling greetLetter calls the returned function from the greet() function
greetLetter(); // logs 'Hi Hammad'
/*< !--- 或者是 --- >*/
greet()();

当直接调用外部函数时,内部的return的函数并不会被调用。所以用一个变量保存外部函数的调用,然后将这个变量当做函数调用。如果不采用这样的方式,可以调用两次外部函数,同样可以调用内部的函数。

共有作用域和私有作用域

在JavaScript中并没有共有作用域和私有作用域的概念,可以借助闭包实现这个特性。熟悉的套路是这样的:

1
2
3
(function(){
//private scope
})();

在其中可以加入变量、方法函数,有些是私有的,不可访问的,有一些我们希望可以是公开能在外部访问到的。可以使用闭包的一种形式,模块模式。

1
2
3
4
5
6
7
8
9
10
var Module = (function() {
function privateMethod() {
// do something
}
return {
publicMethod: function() {
// can call privateMethod();
}
};
})();

在Module这个命名空间下,公有函数可以访问私有函数,这样做的优点是显而易见的,但是缺点也一样,在Module命名空间下,同样可以修改这个私有方法。

使用call、apply、bind改变上下文

关于这部分,之前的文章中已经有过说明,call、apply、bind总结