JavaScript必知33个概念系列:作用域
函数作用域,块级作用域,词法作用域。
前言
我对于作用域一直处于一种感性的认识,有问题也是凭感觉去看。今天就打破这面镜子,看看镜子后面到底是什么。
先说一下我现在的认知水平:
首先函数作用域我是知道的,函数体内部就是函数作用域;
然后块级作用域我大概知道,花括号内部?if、else、for这些?
最后词法作用域?没听过。。
接下来我们来探索一下这些术语(terminology)的语义吧。
作用域
作用域往往和变量挂钩,在谈作用域之前,我们先来看看声明变量的三种方法以及他们之间的区别。
var、let、const
var
用var
声明的对象是属于函数作用域(function scope)的,如果你不在函数作用域下声明,那这个变量就是更高级函数作用域或者全局的(global scope)。
看几个例子:
1 | function setWidth(){ |
因为width
是在函数作用域(setWIdth
函数)下用var
声明的,所以他属于函数作用域,因此全局作用域下访问不到width
1 | var age = 100; |
因为用var
声明的变量dogYears
不是在函数作用域下声明的,因此在本例中会当做全局作用域。
let和const
用let
和const
声明的变量是属于块级作用域的,而不是函数作用域。
块级作用域就是大括号“{}”之间的部分。
类似的下面这个例子,我用let
代替var
来声明dogYears
变量
1 | var age = 100; |
因为使用let
定义dogYears
变量,那么dogYears
变量属于块级作用域,即if的作用域。
函数作用域(function scope)
函数体内部叫函数作用域。
- 用
var
声明的对象是属于函数作用域(function scope)的,如果你不在函数作用域下声明,那这个变量就是更高级函数作用域或者全局的(global scope)
块级作用域(block scope)
大括号“{}”之间称为块级作用域,当然函数作用域也是块级作用域。
- 用
let
和const
声明的变量是属于块级作用域的,而不是函数作用域。(实际上可以看做立即执行函数内部的var
词法作用域(lexical scope)
Variables in JavaScript are lexically scoped, so the static structure of a program determines the scope of a variable (it is not influenced by, say, where a function is called from).
词法作用域就是可以通过源代码来看出哪个变量属于哪个作用域,不会根据函数被调用的上下文改变,词法和静态(static)可以看做是一样的。词法作用域根据声明变量的位置来确定该变量可被访问的位置。
看个例子:
1 | function makeFunc() { |
displayName
中可以访问外部函数makeFunc
的变量name
,而displayName
实际调用时makeFunc
的栈帧已经弹出,此时却还能访问name
。词法作用域是通过静态代码看的,和具体执行无关。这其实是闭包提供的作用。
作用域链
当你在作用域S中访问父作用域的局部变量是可以的,而访问兄弟作用域的局部变量是不可以的。这可以通过作用域链来理解
- 存在一个全局环境,里面存的是全局变量(函数)
- 环境里的函数条目会指向函数对象
- 函数对象通过内部的
[[Scope]]
属性来指向它的作用域 - 函数被调用时,会为此函数作用域创建一个环境,这个环境通过
outer
属性指向父环境 - 作用域会形成一条链
如下图:
函数执行栈是动态的,而作用域链可以看做静态的(或者说是词法的,Lexical)。
相关话题
变量提升(变量声明提升,variables declaration hoisted)
JavaScript将变量的声明提升到该变量的直接作用域下的开始处,变量的赋值并不提升(执行上下文创建时已经为变量分配空间并赋值为
undefined
,这就好像是“变量提升”了)。函数定义也会进行提升,因此作用域下函数定义在前在后没多大区别。
仅对于var
声明的变量进行提升,对const
、let
声明的变量不适用。
看个例子:
1 | function f() { |
函数f()
的实现就好像如下定义一样:
1 | function f() { |
关于“var
是函数作用域的”引起的问题和解决办法
举个例子,如下代码,假如说你不想在if语句后面使用tmp
变量或者说你有一个新的同名的tmp
变量,那你如何解决这个问题呢?
1 | function f() { |
- 方法一:使用
let
(因为let
是块级作用域的 - 方法二:立即执行函数(
IIFE,Immediately Invoked Function Expression
)(利用var
是函数作用域的,ES5
时是一个常见的编程模式。但是效率比较低
1 | function f() { |
关于“立即执行函数的问题”(立即执行函数表达式
一个注意事项
看个例子,如果第一个立即执行函数后面不加分号,那么第二个立即执行函数会当做参数。所以这种情况需要注意加分号
1 | (function () { |
利用prefix operators
解决上述分号问题
1 | !function () { // open IIFE |
如果函数定义已经处于一个表达式中,那么可以直接调用
1 | var File = function () { // open IIFE |