Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之词法作用域和动态作用域 #3

Open
mqyqingfeng opened this issue Apr 23, 2017 · 140 comments
Open

JavaScript深入之词法作用域和动态作用域 #3

mqyqingfeng opened this issue Apr 23, 2017 · 140 comments

Comments

@mqyqingfeng
Copy link
Owner

@mqyqingfeng mqyqingfeng commented Apr 23, 2017

作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

静态作用域与动态作用域

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

让我们认真看个例子就能明白之间的区别:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 ???

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

动态作用域

也许你会好奇什么语言是动态作用域?

bash 就是动态作用域,不信的话,把下面的脚本存成例如 scope.bash,然后进入相应的目录,用命令行执行 bash ./scope.bash,看看打印的值是多少。

value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar

这个文件也可以在 Github 博客仓库中找到。

思考题

最后,让我们看一个《JavaScript权威指南》中的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

猜猜两段代码各自的执行结果是多少?

这里直接告诉大家结果,两段代码都会打印:local scope

原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

而引用《JavaScript权威指南》的回答就是:

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

但是在这里真正想让大家思考的是:

虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

如果要回答这个问题,就要牵涉到很多的内容,词法作用域只是其中的一小部分,让我们期待下一篇文章————《JavaScript深入之执行上下文栈》。

下一篇文章

JavaScript深入之执行上下文栈

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@JSupot
Copy link

@JSupot JSupot commented May 11, 2017

之前一直不太理解什么是词法作用域,受教了

@MissCuriosity
Copy link

@MissCuriosity MissCuriosity commented May 27, 2017

JS的上下文真的是个很神奇的东西,我看的是汤姆大叔的深入理解JS,这本书里面解释的也挺详细的,让我感觉看了之后稍微懂了,但是后面又会忘。
在看你文末两段代码的时候我其实觉得挺奇怪的,因为return f,我认为会返回function f() {};后面再看了一眼,原来checkscope()();也是无奈。

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 27, 2017

@MissCuriosity 我也看过汤姆大叔的深入理解 JavaScript 系列,深受启发和影响。return f 确实会返回 function f(){},只是因为 checkscope()(),返回的 f 函数又被执行了~

@yangshun352607664
Copy link

@yangshun352607664 yangshun352607664 commented May 27, 2017

作者的案列太过简单,应该写那种嵌套比较深的的

@wqxc
Copy link

@wqxc wqxc commented May 29, 2017

谢谢😜

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 29, 2017

@yangshun352607664 哈哈,快来举一个例子~~~

@rccoder
Copy link

@rccoder rccoder commented Jun 1, 2017

核心就是: 函数的作用域在函数定义的时候就决定了

真正理解这句话的时候闭包什么的也就理解了。

Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked. In order to implement lexical scoping, the internal state of a JavaScript function object must in- clude not only the code of the function but also a reference to the current scope chain. (Before reading the rest of this section, you may want to review the material on variable scope and the scope chain in §3.10 and §3.10.3.) This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature. (This is an old term that refers to the fact that the function’s variables have bindings in the scope chain and that therefore the function is “closed over” its variables.)

Technically, all JavaScript functions are closures: they are objects, and they have a scope chain associated with them. Most functions are invoked using the same scope chain that was in effect when the function was defined, and it doesn’t really matter that there is a closure involved. Closures become interesting when they are invoked under a different scope chain than the one that was in effect when they were defined. This happens most commonly when a nested function object is returned from the function within which it was defined. There are a number of powerful programming techniques that involve this kind of nested function closures, and their use has become relatively common in JavaScript programming. Closures may seem confusing when you first en- counter them, but it is important that you understand them well enough to use them comfortably.

JavaScript, The Definite Guide

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Jun 1, 2017

@rccoder 感谢补充!非常赞同,词法作用域其实是非常重要的基础,所以才会作为第二篇去讲解。

@suoz
Copy link

@suoz suoz commented Jun 8, 2017

在全局作用域中“定义”一个函数到时候,只会创建包含全局作用域的作用域链。
只有“执行”该函数的时候,才会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端。

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Jun 8, 2017

@suoz 你剧透了哈~ 😂

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Jun 15, 2017

@rccoder 我搬运了下你的博客中这段的翻译,希望不要介意~

翻译成中文的话也许是这样:

和大多数的现代化编程语言一样,JavaScript是采用词法作用域的,这就意味着函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。为了去实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。函数对象可以通过这个作用域链相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为闭包。

从技术的角度去将,所有的JavaScript函数都是闭包:他们都是对象,他们都有一个关联到他们的作用域链。绝大多数函数在调用的时候使用的作用域链和他们在定义的时候的作用域链是相同的,但是这并不影响闭包。当调用函数的时候闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链的时候,闭包become interesting。这种interesting的事情往往发生在这样的情况下: 当一个函数嵌套了另外的一个函数,外部的函数将内部嵌套的这个函数作为对象返回。一大批强大的编程技术都利用了这类嵌套的函数闭包,当然,javascript也是这样。可能你第一次碰见闭包觉得比较难以理解,但是去明白闭包然后去非常自如的使用它是非常重要的。

通俗点说,在程序语言范畴内的闭包是指函数把其的变量作用域也包含在这个函数的作用域内,形成一个所谓的“闭包”,这样的话外部的函数就无法去访问内部变量。所以按照第二段所说的,严格意义上所有的函数都是闭包。

需要注意的是:我们常常所说的闭包指的是让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。

链接地址: rccoder 博客链接

@double-chen
Copy link

@double-chen double-chen commented Jun 23, 2017

我有一点想不通,在读作者 执行上下文的文章,感觉执行上下文是在函数调用时准备的,作用域规定了如何查找变量。可是查找的变量是通过执行上下文中的变量对象和作用域链查找的,这是动态作用域的原理啊,为什么JS又是静态作用域了。

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Jun 23, 2017

@double-chen 动态作用域和静态作用域,决定的是作用域链的顺序

@yh284914425
Copy link

@yh284914425 yh284914425 commented Aug 1, 2017

var a = 10;
var o = {
     a:11,
     b:{
         fn:function(){
              console.log(a);
         }
     }
}
o.b.fn();

函数包裹函数那种作用域理解了,这样的又有点懵了

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Aug 3, 2017

@yangshun352607664 结果是 10 ,因为变量 a 并不能读取到对象 o 的属性 a ,如果 console.log(o.a),就会打印 11,函数 fn 的作用域链为 [AO, Global.VO],而 Global.VO 中包括了变量 a 和变量 o。

@mengxin-FE
Copy link

@mengxin-FE mengxin-FE commented Aug 14, 2017

var value = 1;
function bar(){
    var value =2;
    console.log(value)
}
bar() //2

为什么这回又返回2了呢? 我还是没理解

@nicewahson
Copy link

@nicewahson nicewahson commented Aug 14, 2017

如果是这样的,function out(function(){console.log(xxx)})这种形式,那括号里的函数作用域是怎么样的?

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Aug 16, 2017

@mengxin-FE 词法作用域决定了变量查找的顺序,这个顺序是从函数内部开始,然后到函数定义的外层,函数内部已经有值,所以就会打印 2

@ShaoHans
Copy link

@ShaoHans ShaoHans commented Jun 19, 2020

作者确实对局部作用域判断有失误,指出错误并且改正就好了,不用说的这么严重,上纲上线!谁没有失误的时候呢,莫慌,淡定~这样也能体现你自己的优秀啊 原始邮件 发件人: ShaoHansnotifications@github.com 收件人: mqyqingfeng/BlogBlog@noreply.github.com 抄送: Subscribedsubscribed@noreply.github.com 发送时间: 2020年6月19日(周五) 14:42 主题: Re: [mqyqingfeng/Blog] JavaScript深入之词法作用域和动态作用域 (#3) @你好,我是通过截图中的推荐阅读找到的你这篇文章。截图中的代码和你的代码机几乎是一样的,只是value的值和书写的位置不一样,但都是全局变量。但截图中说答案是3,而且也给出了解释。但我实际执行了一次,结果是2,如你解释的一样。这篇文章是不是误人子弟啊,原文链接:http://es.xiecheng.live/es6/scope-let-const.html#%E4%BD%9C%E7%94%A8%E5%9F%9F — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.

嗯嗯,没办法联系到作者,我也很想给他改掉。

@js100cc
Copy link

@js100cc js100cc commented Jun 23, 2020

this应该就是典型的动态作用域吧.

this 具有类似动态作用域的特性,但不是典型动态作用域。动态作用域基于 callstack,作用域链单向的像下层 frame 引用。而 this 只寻址当前函数(非箭头函数),根据函数执行方式由引擎传从 heap 取值传入 frame。

作为对比,JS 词法作用域是基于 heap 的,作用域关系不受 callstack 的影响,即任何时候都按源代码词法结构生成词法环境底层数据结构(放在 heap 中)。作用域链根据词法环境的单向引用关系。这是 JS 实现闭包的基础。

@afishhhhh
Copy link

@afishhhhh afishhhhh commented Jul 14, 2020

@mqyqingfeng 但是两种写法都能正常运行哦, 按词法作用域的理解应该是输出undefined

如果logA() 在 const a = 1; 之前执行就是输出 undefined,按照顺序执行时作用域就已经确定了

这样的话会报 ReferenceError

@70hnXX
Copy link

@70hnXX 70hnXX commented Aug 20, 2020

var value = 1;
function bar(){
    var value =2;
    console.log(value)
}
bar() //2

为什么这回又返回2了呢? 我还是没理解

console.log(value)是一个函数啊,他定义的时候所在的作用域是在bar()的内部,然后在这个作用域内查找value,就查找到value = 2了.

@70hnXX
Copy link

@70hnXX 70hnXX commented Aug 20, 2020

如果第二次不重新声明value的话,输出将会是2


var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    value = 2;
    foo();
}
bar();//2

没毛病啊,你修改了全局变量value的值,再打印value,当然是2了.

@tuyongtao-T
Copy link

@tuyongtao-T tuyongtao-T commented Aug 20, 2020

var value = 1;
function bar(){
    var value =2;
    console.log(value)
}
bar() //2

为什么这回又返回2了呢? 我还是没理解

console.log(value)是一个函数啊,他定义的时候所在的作用域是在bar()的内部,然后在这个作用域内查找value,就查找到value = 2了.

如果第二次不重新声明value的话,输出将会是2


var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    value = 2;
    foo();
}
bar();//2

没毛病啊,你修改了全局变量value的值,再打印value,当然是2了.

这里bar函数内是 value = 2;没有用var声明,自动成为全局变量,重写了value的值,所以打印的的2

@anjina
Copy link

@anjina anjina commented Oct 22, 2020

`
var x = 21;

var talk = function () {

    console.log(x);

    var x = 20;

};

talk ();

`
这个怎么用词法作用域解释下呢

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

talk 函数执行时的作用域链为 [talk.AO, globalContext.VO], 在执行上下文的准备阶段,会进行活动对象AO的初始化,活动对象包括[ arguments, x=undefined] ; 在执行上下文的执行阶段,执行console.log时, 在talk的作用域中就找到了x, 所以直接打印 此时没有值, 打印为undefined

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

闭包能理解,前面的静态作用域的例子不大能理解。。。为啥foo()中没找到value之后,就去最外层找了?不应该去bar()中找吗?求助大神们

作用域在函数创建时就确定了,这就是词法作用域

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

但是两种写法都能正常运行哦, 按词法作用域的理解应该是输出undefined

词法作用域规定的是函数创建时候的作用域,函数logA的作用域链为[logA.AO, globalContext.VO],在函数执行时,外层作用域已经定义并赋值了 变量a ,所以logA console.log 查找a时,在当前作用域没有,在外层找到了 ,输出a

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

@mqyqingfeng 但是两种写法都能正常运行哦, 按词法作用域的理解应该是输出undefined

如果logA() 在 const a = 1; 之前执行就是输出 undefined,按照顺序执行时作用域就已经确定了

这个 应该是会报错的, const 定义变量,存在暂时性死区, 不能在声明前访问

@youjiaqi421
Copy link

@youjiaqi421 youjiaqi421 commented Nov 5, 2020

一个函数是一个作用域 因为js词法作用域,所以当第一个foo函数Ao对象没有对应的值的时候回去window上寻找也就是Go对象上寻找,而动态语法就是函数在那个函数体里运行,并且所处的作用域也就是这个函数的作用域,

@iszhangjin
Copy link

@iszhangjin iszhangjin commented Jan 17, 2021

var a = 10;
var o = {
     a:11,
     b:{
         fn:function(){
              console.log(a);
         }
     }
}
o.b.fn();

函数包裹函数那种作用域理解了,这样的又有点懵了

es6之前只有全局作用域和函数作用域,后面有了局部作用域。你这里声明一个具有属性a的对象o,其实并没有创建作用域

@moxaIce
Copy link

@moxaIce moxaIce commented Mar 21, 2021

在全局作用域中“定义”一个函数到时候,只会创建包含全局作用域的作用域链。
只有“执行”该函数的时候,才会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端。

console.log(value)是一个函数啊,他定义的时候所在的作用域是在bar()的内部,然后在这个作用域内查找value,就查找到value = 2了.

console.log(value) 这个算执行吧,不是说定义的时候决定的吗,他执行的时候怎么读到value为2的呢

@alan89757
Copy link

@alan89757 alan89757 commented Apr 15, 2021

  var val = 10;
  foo();
  var val = 20;
  function foo() {
    console.log(val);  // 10
  }

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

大佬,按照词法作用域说法,应该打印的是20,实际打印的是10,这是为什么?

@Popxie
Copy link

@Popxie Popxie commented Apr 16, 2021

@yangshun352607664 哈哈,快来举一个例子~~~

怼他 哈哈哈啊哈哈,眼高手低

@InsHomePgup
Copy link

@InsHomePgup InsHomePgup commented May 21, 2021

第二段: checkscope的例子里都没有console语句。。。怎么知道是在哪里打印的呢?

@LeonaYoung
Copy link

@LeonaYoung LeonaYoung commented May 31, 2021

@yangshun352607664 结果是 10 ,因为变量 a 并不能读取到对象 o 的属性 a ,如果 console.log(o.a),就会打印 11,函数 fn 的作用域链为 [AO, Global.VO],而 Global.VO 中包括了变量 a 和变量 o。

[AO, Global.VO] 请问这是什么写法啊?看不太明白

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet