深入学习作用域和闭包—全面(JS系列之二)

作用域

在学习作用域之前,先了解两个重要的概念:编译器、引擎

编译器:负责词法分析及代码生成等编译过程

引擎:负责整个 JavaScript 程序的编译和执行

什么是作用域

通俗的来讲就是变量起作用的范围。比较规范的解释(引用《你不知道的 JavaScript 》上卷),负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行代码对这些标识符的访问权限。

ES6之前,JavaScript只有全局作用域函数作用域,与其他类型语言不同的是它没有块级作用域。

if(true){
    var a = 1;//全局作用域
}
console.log(a); // 1

function foo(){
  var b = 1;//函数作用域
    console.log(a); //1
}
console.log(b); // ReferenceError 

在上面的代码中,a 属于全局作用域,if 后的花括号并没有形成块级作用域,而 b 属于 foo 函数的作用域,在JavaScript中函数外部作用域访问不到函数内部作用域,所以在全局作用域中访问foo函数作用域变量b会报错。

es6之后,JavaScript 拥有了块级作用域

if (true) {
    let a = 1
}
console.log(a)   // ReferenceError 

ifforwhiletry...catch 等在大括号中使用letconst 声明的变量会形成块级作用域,如果在外部访问会报错。

作用域如何工作

变量提升

刚开始接触 JavaScript 的同学可能会对变量先声明后使用的现象十分不解,要理解它我们得了解JavaScript编译的两个原则:①编译时声明 ②运行时赋值

var a = 2;

//相当于↓
var a; //编译时
a = 2; //运行时

上面这段代码 var a = 2只做一件事,对a进行赋值 ,不过浏览器引擎不这么看, 它会被分为 var aa = 2 两步进行,一个在编译器编译时声明变量,另一个在引擎运行时赋值。

编译器首先将上面这段程序分解为词法单元,然后将词法单元解析成一个树结构(AST抽象语法树)。在开始代码生成时,编译器遇到var a,编译器询问作用域是否已经声明了这个变量;如果是,编译器忽略该声明,否则在当前作用域集合声明一个新的变量,命名为a

引擎执行a = 2首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量,否则引擎会继续延着作用域链查找该变量。如果引擎最终找到了a变量,就会将 2 赋值给它,否则引擎会抛出一个异常Uncaught ReferenceError: a is not defined

函数提升

a()  // aaa => 函数a被提升,所以在声明前可以调用函数

var a
function a () {
console.log('aaa')
}

console.log(a) // ƒ a() {} 函数声明优先级比变量声明高

var声明的变量会提升,function 声明的函数也会被提升,并且函数声明优先级比变量声明优先级高,所以上面这段代码打印 a 是个函数,因为var a声明的变量被function声明的函数覆盖了。

词法作用域

词法作用域就是定义在词法阶段的作用域,也就是说作用域是在书写代码时函数声明的位置来决定,与执行过程无关,JavaScript 采用的是词法作用域。

相对词法作用域另外一种叫做动态作用域,作用域是在执行阶段确定的,比如Bash脚本、Perl语言等。

看下面这段代码示例:

var a = 1

function foo () {
console.log(a)
}
function bar () {
    var a = 'local'
    foo ()
}

bar() // 词法作用域是:1 ;动态作用域是:‘local’


我们使用词法作用域和动态作用域分析一下上面这段代码执行过程,bar 函数内部调用 foo 函数

如果是词法作用域,调用 foo 查找变量a会从foo函数代码定义的位置向外一层也就是全局作用域访问,此时var a = 1,结果是 1;

如果是动态作用域,调用foo查找变量a会从当前调用函数位置开始向往搜索,发现外部声明var a = 'local',所以 a的值是local;

而在JS引擎中上面这段代码运行结果是 1,所以JavaScript采用的是词法作用域

不过,thisJavaScript 中比较特殊,JavaScript 程序在执行的时候才会对this进行赋值,在未执行时不能知道this的作用域,所以比较准确的说在JavaScriptthis采用的是动态作用域。

修改词法作用域: eval 和 with

eval 欺骗词法作用域

eval 函数接收一个或多个声明的代码,会修改其所处的词法作用域。


var a = 2
function foo (str, b) {
    eval(str) // 欺骗
    console.log(a, b)
}
foo('var a = 3', 1) // 3, 1

执行 eval 函数,传入的字符串会解析成脚本执行,声明一个变量 a 修改了 foo 函数的词法作用域,遮蔽了外部(全局)作用域中的同名变量访问,欺骗了 foo 词法作用域。另外,使用 eval 函数还容易受到xss攻击。

with 欺骗词法作用域

with 将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,如果对象中没有该标识号,会在全局创建一个新的词法作用域

with 的用法

var obj = {
    a: 1,
    b: 2,
    c: 3
}
// 对象属性赋值,多次使用obj
obj.a = 2
obj.b = 3
obj.c = 4

// 使用 with 写法简洁
with(obj) {
    a = 3;
    b = 4;
    c = 5;
}

with 的缺陷

function foo(obj) {
    with(obj) {
        a = 2
    }
}
var obj1 = {
    a: 3
}
var obj2 = {
    b: 3
}
foo(obj1)
console.log(obj1.a) // 2

foo(obj2)
console.log(obj2.a) // undefined
console.log(a) // 2 —— a被泄露到了全局作用域上

with 会修改引用中属性的值,如果引用中没有该属性,在非严格模式下会在全局作用域中创建一个全新的词法作用域,欺骗了全局词法作用域

除此之外,使用 evalwith 还会带来性能问题,因为JS 引擎无法在编译时对它们作用域进行查询优化,这样会导致代码运行效率变慢,所以建议不要使用它们。

作用域链

作用域链形成是由词法作用域和编译时词法环境对外部环境引用的结果,关于词法环境外部环境的引用可以参考这篇文章【深入了解JavaScript执行过程】
)

现在主要说说作用域链的构成过程,开始执行脚本时创建全局作用域,在全局环境调用 foo函数 时,编译foo 函数并创建foo函数作用域,foo 函数中声明 bar函数,在调用 bar函数会创建 bar 函数作用域。JavaScript中,内部函数可以访问外部函数的变量,这样, bar 函数作用域 =》 foo 函数作用域 =》全局作用域 构成了一条作用域链。


var a = 'global'
function foo () {
    var b = 'foo scoped'
    function bar () {
        var c = 'bar scoped'
        console.log(a, b, c)
        }
    bar()
    }
}


foo() // 'global'    'foo scoped'     'bar scoped'
    

闭包

谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js工程师实力的一个重要指标。下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义闭包

问题如下:

- 什么是闭包

- 闭包的原理是什么

- 闭包是如何使用的

- 闭包的应用场景有哪些

如果你能回答上面这些问题,说明你对闭包非常熟悉了;如果脑子里比较模糊回答不上来也不用担心,继续往下读,相信你会找到答案的。

什么是闭包

网上有很多种对闭包解释的说法:

1、闭包是由函数以及创建该函数的词法环境组合而成

2、闭包是能够读取其他函数内部变量的函数

读起来比较抽象和拗口,用代码来理解闭包。

function foo() {
    var a = 2
    function bar () {
        console.log(a)
    }
    return bar
}
var baz = foo()

baz() // 2 —— 这就是闭包的效果

image

函数是一等公民,可以当成数值来使用,它既可以作为函数参数,也可以作为函数返回值。调用foo函数返回bar,理论上来说foo函数执行完之后会被销毁,不过bar函数引用着fooa变量,所以执行完foo,函数体会被销毁,但是a被引用着不能被回收仍然保存在内存当中,所以在外部调用bar函数可以访到foo内部函数的a变量。这时我们给foo起了另外一个名字叫闭包函数。

我们知道根据作用域链函数内部可以访问函数外部的变量,反过来是不行的,但是闭包可以做到,这就是闭包的神奇之处

总结一下,闭包本质上是一个函数,它返回另一个函数,可以使外部函数可以访问其他函数内部的变量。

闭包原理

细心的朋友可能知道答案了,闭包的原理就是词法作用域和作用域链形成的结果。

如何使用闭包

为了能让我们的程序更健壮,我们往往需要将实现细节隐藏起来,只对外提供暴露接口,这也是面向对象三大特性之一封装性

私有变量

function foo () {
    var num = 0
    function bar () {
        ++num
        return num
    }
    return bar
}
var add1 = foo ()
add1() // 1
add1() // 2
add1() // 3
var add2 = foo ()
add2() // 1
add2() // 2
add2() // 3

每次执行foo都得到相同的值,不会相互污染

function Person() {
    var age = 20
    var sex = 'man'
    getAge () {
        return age
    }
    setAge(value) {
        age = value
    }
    getSex () {
        return sex
    }
    setSex(value) {
        sex = value
    }
    return {
        getAge,
        setAge,
        getSex,
        setSex
    }
}

var zhangsan = Person()
zhangsan.getAge() // 20
zhangsan.getSex() // 男

隐藏实现细节,对外暴露接口。模拟实现了面向对象的思想,代码也显得健壮、易理解、可扩展可维护。

闭包的应用场景

定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包

闭包使用注意事项

1、闭包会使得函数中的变量都被保存在内存中,内存消耗很大,处理不当,容易造成内存泄漏

2、如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

总结

写的内容有点多,梳理一下

1、首先讲了什么是作用域,作用域类型分为全局作用域、函数作用域、函数作用域

2、其次作用域工作时,使用varfunctioin声明会出现变量提升和函数提升;JavaScript 是词法作用域,evalwith 会欺骗词法作用域

3、最后讲了作用域链的原理和闭包使用介绍

引用链接

深入javascript——作用域和闭包

JavaScript中的作用域和闭包

从作用域链谈闭包

【第863期】深入学习JavaScript闭包

推荐阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容