关于JavaScript中的作用域

声明:本文引用了《你不知道的JavaScript(上卷)》一书作用域篇章的部分代码示例和文字描述

文章的开始,我们先提出这么几个问题:

  • 作用域是什么?
  • 作用域的工作模式?
  • 作用域有哪几类?
  • 什么情况下会产生作用域?
  • 作用域有什么用,或者说如何利用?

作用域是什么

作用,标识符被访问或调用, 域,空间、范围的意思,如最简单的代码 var a = '123' ,a变量保存在哪里(此处保存在变量环境)?后续的操作中如何找到它?

这些问题,需要一套规则来存储变量,以及定义如何查找它们,这套规则就叫 作用域,而完整的查找链条则被称为作用域链

关于变量环境 + 词法环境,大家有兴趣可以另行查找资料学习

function foo(a) {
  var secondName = '铁锤'
  sayHellow()
  function sayHellow() {
    console.log('hello i am ' + firstName + secondName)
    console.log(message) // ReferenceError: message not defined
  }
} 

var firstName = '李'
foo('铁锤')
代码示例中的作用域嵌套气泡图(方的气泡 -_- !)

当执行sayHello()中的console.log('hello i am ' + firstName + secondName), 需要对 firstName + secondName 两个变量执行RHS查找(相对的,还有LHS,区别是此时变量是赋值操作的目标(LHS),还是被赋值操作使用的值(RHS))

  • firstName: sayHello() 中找不到,则往上/往外查找到foo(),也没找到,继续往上查找,OK,在全局作用域找到了
  • secondName: 同上
  • message: 同样的逻辑,直到全局作用域都没有找到,抛出 ReferenceError

作用域的工作模式

作用域分两种工作模型,一种是我们常见的词法作用域,大部分编程语言使用的也是这种(当然包括JavaScript),

  • 词法作用域:大部分编程语言使用这种,在词法分析阶段决定,因此可以理解为静态作用域
  • 动态作用域:Bash脚本(如git bash),Perl中的一些模式(如shell命令),

词法作用域

说到词法作用域,可以先说明一下JavaScript的编译3个阶段

  • 分词/词法分析:把一行代码分解成代码块(词法单元),如var a = 2;会被分解成var, a, =, 2, ;
  • 解析/语法分析:将上一个步骤生成的词法单元流转换成一颗逐级嵌套的的树,也就是AST抽象语法树(Abstract Syntax tree)
  • 代码生成:将AST语法树转换成一组机器指令:声明一个叫做a的变量,并为它分配内存,然后把2这个值储存在a中

词法作用域是定义在编译-词法分析阶段的作用域,词法分析的对象是你的源代码,也就是说词法作用域由你写代码时将变量和块作用域写在哪里来决定的,因此词法分析器处理代码时会保持作用域不变(大部分情况如此,除了eval + with)

function foo(a) {
    var b = a * 2
    function bar(c) {
        comsole.log(a, b, c)
    }
    bar(b*3)
}
foo(2)
// 1、全局作用域:一个标识符:foo
// 2、foo函数作用域:3个标识符: 形参a, b, bar
// 3、bar函数作用域:1个标识符:形参c

欺骗词法(eval +with)

eval

eval函数可以接收一个字符串参数,并把该字符串当做可执行代码进行执行,字符串参数是动态的,所以其执行过程中可做变量声明,或者变量修改,因此eval函数所处的外部函数的作用域有可能会被修改

function foo(str) {
    eval(str)
    console.log(msg) // 我在eval函数中被声明
}
var msg = '我在全局作用域'
foo('var msg = "我在eval函数中被声明"')

注意:在严格模式中,eval(...)有自己的作用域,并不会影响到所处作用域

function foo(str) {
    "use strict"
    eval(str)
    console.log(msg) // ReferenceError: a is not defined
}
foo('var msg = "尝试修改所处作用域"')

with

with通常被当做重复引用同一个对象的多个属性的快捷方式,可以不用重复引用对象本身

var obj = {a: 1, b: 2, c: 3}
// 正常情况下挨个属性赋值
obj.a = 11
obj.b = 12
obj.c = 13
// 使用with
with(obj) {
    a = 11
    b = 12
    c = 13
}

with可以将传入的对象处理成完全隔离的词法作用域,而它的属性则自动处理为定义在该作用域内的词法标识符,但是这个块内部正常的var 声明并不会被限制在块内部,而是被添加到with执行时所处的作用域中

function foo(obj) {
    with(obj) {
        a = 2
    }
    // console.log(a) // foo(o2) 时打印 2,
}
var o1 = {
    a: 3
}
var o2 = {
    b: 4
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 2 a被泄漏到了全局作用域(LHS 导致的,严格模式的话,将会阻止在全局作用域声明)

性能

eval可以动态执行JavaScript代码,with可以方便访问对象属性,看起来都是非常棒的特性,但是,JavaScript引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于能够根据代码的词法分析,来预先确定所有的变量和函数的定义位置,并在执行过程中快速找到标识符。因此,如果引擎在代码中发现了eval(...) / with(...),JS引擎出于正确性和严谨性的考虑,它只能认为自己做的优化是无效的( 这两者都可能会改变所处的作用域甚至全局作用域),因此运行效率将会降低。

作用域有哪几类

在JavaScript中作用域分为两类:

  • 函数作用域
  • 块级作用域

函数作用域

每声明一个函数,JS引擎都会为它创建一个函数作用域,属于这个函数的所有变量都可以在整个函数范围被访问(也包括函数体中嵌套的作用域),当发生变量查找时,从代码所属作用域开始查找,当前作用域查找不到时,逐层往外查找,最终查找到全局作用域的查找逻辑,也就是所谓的 作用域链

隐藏内部实现

在Java中有的属性可以被定义为private(私有属性),它只属于当前对象,并且不希望被外界其他对象访问。这就是最小特权原则,也叫最小授权 或者 最小暴露 原则,一个对象或者方法的设计,应最小限度地暴露必要内容,比如某个模块或者对象的API 设计

// bad
var eatWhat = '吃什么'
var drinkWhat = '喝什么'
function haveFun() {
    letUsHappy()
}
function letUsHappy() {
    console.log(eatWhat)
    console.log(drinkWhat)
}

// good : 该是我的变量和方法,都处在我自己的函数作用域当中,类似外界无法访问我的私有属性和方法
function haveFun() {
    var eatWhat = '吃什么'
    var drinkWhat = '喝什么'
    function letUsHappy() {
        console.log(eatWhat)
        console.log(drinkWhat)
    }
    letUsHappy()
}

避免同名标识符冲突

这个根据作用域链的查找规则就比较好理解了,也就是说如果当前作用域内已经可以查找到对应的标识符,标识符查找也就不会再往外查找了

var name = '我是全局作用域的名字'
function foo() {
    var name = '我是foo 函数内部的名字'
    console.log(name)
}
foo()

块作用域

首先块的概念是什么,JavaScript中有哪些块?

if() {} // if块
while() {} // while块
{} // 独立的代码块
for() {} // for循环的迭代块

由此,我们可以粗暴地理解为{...}就是一个块,不过这并不意味着这里面就有块级作用域了,在ES6let关键字之前,我们可以认为JavaScript是没有块级作用域的概念的,除了以下几个奇怪的兄弟

// with 块
var obj = {a: 1}
with(obj) {
    a = 2 // 此处有块级作用域,外部无法访问a变量
}
// try/catch 的catch分句
try {
    // doSomething error
} cathc(error) {
    console.log(error) // error只能在该分句内访问
}
console.log(error) // ReferenceError: error not defined

let / const

let / const是es6新增的变量声明方式(保存在词法环境),用于声明一个局部变量,使用let / const关键字声明的变量会隐式劫持声明所处的块,被声明的变量只能在该块内被访问或者修改,由此就形成了块级作用域

{
    let a = '我处在块级作用域'
    const b = '我也处在块级作用域'
}
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) // Uncaught ReferenceError: b is not defined

const 用于声明一个常量,该变量的值是不可以修改的,不过当声明的变量的值是引用类型时,稍微有点怪异

const a = '1'
const b = {name: 'white'}

b.name = 'black' // 正常 b是引用类型,b.name修改的是被引用的值,而没有修改b(内存指针)本身
b = {name: 'white'} // Uncaught TypeError: Assignment to constant variable.
a = '2' // Uncaught TypeError: Assignment to constant variable.

你可能注意到了,上面两份代码示例中,错误类型是不一样的:ReferenceError vs TypeError

  • ReferenceError:reference(引用),这种错误发生在标识符查找出错时,或者尝试去访问一个未定义的变量时(此处需注意变量提升,也就是声明提前)
  • TypeError:变量查找已经完成,但是执行了错误的操作,如调用了不存在的方法var msg = 123;msg.forEach(...)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容