mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。
30.76k stars 4.71k forks source link

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

Open mqyqingfeng opened 7 years ago

mqyqingfeng commented 7 years ago

作用域

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

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

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,对作者也是一种鼓励。

xiaofan9 commented 6 years ago

@lzuliuyun 很好奇的是词法分析(词法作用域)这个阶段是发生在js编译阶段,还是执行阶段

lzuliuyun commented 6 years ago

@xiaofan9 词法分析是在编译阶段决定的,也就是说,在执行之前就已经确定了作用域的范围。你可以阅读下作者的“JavaScript深入之变量对象”:https://github.com/mqyqingfeng/Blog/issues/5

xie991283109 commented 6 years ago

大佬,你后面举的javascript权威指南的两个例子,看着就是闭包呀,最终打出‘local scope‘从闭包也可以解释的通,能否把闭包和词法作用域打通说一下呢,感觉还是有点迷啊,谢谢

weiyuu commented 6 years ago

基础的知识只有自己啃书才能理解

发自魏宇的 iPhone

On Aug 22, 2018, at 11:14, xie991283109 notifications@github.com wrote:

大佬,你后面举的javascript权威指南的两个例子,看着就是闭包呀,最终打出‘local scope‘从闭包也可以解释的通,能否把闭包和词法作用域打通说一下呢,感觉还是有点迷啊,谢谢

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or mute the thread.

slogeor commented 6 years ago

@yuu95 看权威指南就行?

jiayousuda commented 6 years ago

你好,,我想问下,,实际项目中一般尽量写成局部变量还是全局变量,,

xiaofan9 commented 6 years ago

@jiayousuda 建议局部

findpikachu commented 6 years ago

你说错了,JavaScript即是词法作用域又是动态作用域,动态作用域指的是匿名函数的this和argument

1011cat commented 6 years ago

你好,其实在最早之前学习js时就有这样的疑问,JavaScript里的作用域是对象吗?感觉不是,但是有这样一句话"JavaScript里万物皆对象".最近读<你不知道的JavaScript>其中有这样一句话"在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。"所以更加迷惑了,请问,你是怎么理解的?谢谢!

qiqingfu commented 6 years ago

@yh284914425

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

你这个例子全局看下来只有两个作用域。一个是全局作用域(Global), 一个是 o.b.fn引用的这个函数作用域。 变量对象(VO): 变量对象是与执行上下文相关的数据作用域。它是与上下文关联的特殊对象,用于存储被定义在上下文中的变量函数声明。 而 Global的变量对象(VO)只有两个。
a: 10 b: object

在一个函数的上下文中有变量对象(VO)还有活动对象(AO)和[[Scope]]作用域链。

活动对象(AO):
在一个函数上下文中,变量对象(VO)被表示为活动对象(activation object) AO, 活动对象(AO)相比变量对象(VO),还可以包含特殊对象arguments。

所以o.b.fn对应的函数中的活动对象是空的。因为既没有参数arguments,也没有变量声明,更没有函数声明。 那么在o.b.fn中打印 console.log(a)。这个函数自身没有,就会去它的父作用域(Global | 全局作用域)中查找。你再看一下它父作用域中的变量对象中只保存两个属性。a: 10 b:object。 所以打印10

个人见解,如有错误麻烦勘正。

Nanchenk commented 5 years ago

JS也存在动态作用域吧,with,try/catch的catch子句好像就是动态的吧,包含eval的代码也不能由定义时确定。

wd2010 commented 5 years ago

@Nanchenk 我也产生了同样的疑惑

var m=1
function foo(){
  console.log(m);//2
}
try{
  var m=2
  foo()
}catch(e){
  console.log(e)
}

在try/catch中若是静态编辑的话,那按理foo输出来的m应该是1,但这里是2 。

@mqyqingfeng 冴羽大大,麻烦指导下

xiaofan9 commented 5 years ago

@wd2010 你这个结果完全没问题啊,在foo调用前,重新声明了m,并赋值为2,foo调用的时候拿到的确实是2

wd2010 commented 5 years ago

@xiaofan9 我被try/catch给迷惑了,es5中他不会产生块级作用域

Nanchenk commented 5 years ago

@xiaofan9 我被try/catch给迷惑了,es5中他不会产生块级作用域

catch中的作用域就是块级的,你的例子不对没体现出来

wd2010 commented 5 years ago

@Nanchenk es5中catch作用域不是块级的,比如

(function(){
  e="default";
  try{
    throw "test";
  }catch(e){
    var e,x=123;
    console.log(e); //test
    console.log(delete e); //false
    e=456;
    console.log(e); //456
  };
  console.log(x); //123
  console.log(e); //default
  console.log(window.e); //undefined
})();
xiaofan9 commented 5 years ago

@Nanchenk es5 没块级作用域的概念

webXLing commented 5 years ago

` var x = 21;

var talk = function () {

    console.log(x);

    var x = 20;

};

talk ();

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

` var x = 21;

var talk = function () {

    console.log(x);

    var x = 20;

};

talk ();

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

这个是因为var 是具有变量提升的, 会在函数顶部 先声明变量 然后在去赋值 ,console 的位置在声明之后 赋值之前所以打印出来时undefined

Jemair commented 5 years ago

es5 的 try 是没有块级作用域的 所以在 try 里面的 var 修改了外部的 m 的值 同样的情况下在 es6 下,由于产生了块级作用域,输出就符合预期了

const m=1
function foo(){
  console.log(m);//1
}
try{
  const m=2
  foo()
}catch(e){
  console.log(e)
}
xiaoxiongzi commented 5 years ago

关于静态作用域与动态作用域,《你不知道的JavaScript》上册讲的很详细

AnsonZnl commented 5 years ago
var value = 1;
function bar(){
    var value =2;
    console.log(value)
}
bar() //2

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

哈哈 ,兄弟,我怀疑你这是看文章看的走火入魔了...

imaxing commented 5 years ago

调用 bar, 进入 bar 的执行上下文向上查找 value, 先找到函数内部的变量 所以打印2

葛星

imaxing@126.com | 签名由网易邮箱大师定制

在2019年01月10日 18:21,张宁乐notifications@github.com 写道: var value =1;

functionbar(){

var value =2;

console.log(value)

}

bar() //2

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

哈哈 ,兄弟,我怀疑你这是看文章看的走火入魔了...

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

xiaofan9 commented 5 years ago

@SageWu 你只执行function f() {return scope}; var scope; f(); 这段代码你就会发现 打印出来的确实是undefined image

SageWu commented 5 years ago

@xiaofan9 谢谢,最终发现,竟然是我用的那工具缓存了之前的赋值。

imaxing commented 5 years ago
当调用 f 的时候 已经执行了 scope ="local scope"; 这就相当于在调用 f 的时候 window 下已经有了scope并且值为'local scope’, 在 f 执行的时候内部查找没找到, 继续向上查找到 window 上下文 就找到了' local scope' 葛星

imaxing@126.com | 签名由网易邮箱大师定制

在2019年01月15日 00:09,SageWunotifications@github.com 写道:

@thisisandy 这段代码确实是返回 "local scope",因为是根据函数创建的位置,然后向外查找变量,自然是 'local scope'

说错了…… 应该是 undefined,因为有函数提升和变量提升,相当于

functionf() {return scope}; var scope; f(); scope ="local scope";

我的运行结果是返回"local scope",这让我觉得很是奇怪,明明f()先于scope的赋值。

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

zhoubhin commented 5 years ago

看了好几遍,基本上是看懂了……

Saitmob commented 5 years ago

image 函数声明提升,所function foovar value = 1之前导致undefined的吧,怎么会是1呢

zhoubhin commented 5 years ago

image 函数声明提升,所function foovar value = 1之前导致undefined的吧,怎么会是1呢

为何我输出的结果是: 1 undefined

comeon-heqiang commented 5 years ago

看了好几遍,基本上是看懂了……

基本懂了,那么很快就忘了

clChenLiang commented 5 years ago

这个跟 context、this 蛮容易弄混的。this 跟 context 都是指运行时的上下文,是跟运行时有关的参数。所以,一般在 定义 的时候使用 that = this 来保留上下文

zhoubhin commented 5 years ago

看了好几遍,基本上是看懂了……

基本懂了,那么很快就忘了

所以还得彻底搞懂才行啊~🤦‍♂️

Sphinm commented 5 years ago

此处的 undefined 是由于函数没有 return 返回值导致的...

在 2019年3月6日,下午1:19,Salvador Zhou notifications@github.com 写道:

https://user-images.githubusercontent.com/22002351/53856044-3df82000-400b-11e9-9767-0869a71d98e2.png 函数声明提升,所function foo在 var value = 1之前导致undefined的吧,怎么会是1呢

为何我输出的结果是: 1 undefined

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/mqyqingfeng/Blog/issues/3#issuecomment-469972210, or mute the thread https://github.com/notifications/unsubscribe-auth/APuGepXekQN7kuhhf3nfcAONnqCRC-aIks5vT0_ZgaJpZM4NFWFZ.

winfredwyw commented 5 years ago

个人觉得 javascript 只有词法作用域,执行上下文 this 不能称之为动态作用域

zhoubhin commented 5 years ago

此处的 undefined 是由于函数没有 return 返回值导致的... 在 2019年3月6日,下午1:19,Salvador Zhou @.***> 写道: https://user-images.githubusercontent.com/22002351/53856044-3df82000-400b-11e9-9767-0869a71d98e2.png 函数声明提升,所function foo在 var value = 1之前导致undefined的吧,怎么会是1呢 为何我输出的结果是: 1 undefined — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#3 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/APuGepXekQN7kuhhf3nfcAONnqCRC-aIks5vT0_ZgaJpZM4NFWFZ.

最后为何输出undefined这个我知道的,我在这边其实是为了告诉楼上那位朋友 @Saitmob ,那段代码最终输出结果为1,而非他认为的undefined。

Saitmob commented 5 years ago

此处的 undefined 是由于函数没有 return 返回值导致的... 在 2019年3月6日,下午1:19,Salvador Zhou @.***> 写道: https://user-images.githubusercontent.com/22002351/53856044-3df82000-400b-11e9-9767-0869a71d98e2.png 函数声明提升,所function foo在 var value = 1之前导致undefined的吧,怎么会是1呢 为何我输出的结果是: 1 undefined — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#3 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/APuGepXekQN7kuhhf3nfcAONnqCRC-aIks5vT0_ZgaJpZM4NFWFZ.

最后为何输出undefined这个我知道的,我在这边其实是为了告诉楼上那位朋友 @Saitmob ,那段代码最终输出结果为1,而非他认为的undefined。

emmm,因为只有一个输出,所以。。明明之前一直都知道那个undefined并不是console.log出来的。。话说怎么会只有一个undefined啊?

zhoubhin commented 5 years ago

此处的 undefined 是由于函数没有 return 返回值导致的... 在 2019年3月6日,下午1:19,Salvador Zhou @.***> 写道: https://user-images.githubusercontent.com/22002351/53856044-3df82000-400b-11e9-9767-0869a71d98e2.png 函数声明提升,所function foo在 var value = 1之前导致undefined的吧,怎么会是1呢 为何我输出的结果是: 1 undefined — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#3 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/APuGepXekQN7kuhhf3nfcAONnqCRC-aIks5vT0_ZgaJpZM4NFWFZ.

最后为何输出undefined这个我知道的,我在这边其实是为了告诉楼上那位朋友 @Saitmob ,那段代码最终输出结果为1,而非他认为的undefined。

emmm,因为只有一个输出,所以。。明明之前一直都知道那个undefined并不是console.log出来的。。话说怎么会只有一个undefined啊?

我也觉得奇怪,不是应该正确地输出最终结果 1 嘛,咋会只有一个undefeated……

Tmac-1 commented 5 years ago

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

function bar() { var value = 2; foo(); } var value = 1; bar(); 为什么这个也会打印1啊。

pinguo-luozhijian commented 5 years ago

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

function bar() { var value = 2; foo(); } var value = 1; bar(); 为什么这个也会打印1啊。

这个代码相当于: var value;

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

function bar() { var value = 2; foo(); } value = 1; bar(); 1是foo中打印出来的 foo是在调用bar中调用的 而bar是在value = 1之后调用的 所以打印出来是1 函数的作用域在创建的时候已经确定了
foo的作用域跟 bar同级 foo中 没有value 所以 沿着作用域链网上找 就找到 顶层的 value = 1

yenava commented 5 years ago

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

最后这个例子 返回的f是什么呀? checkscope后面为什么是接两个()?

zhoubhin commented 5 years ago

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

最后这个例子 返回的f是什么呀? checkscope后面为什么是接两个()?

执行checkscope最终输出checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } 执行checkscope();最终输出f(){ return scope; } 执行checkscope()();最终输出"local scope" 因为每一个的最终执行结果都是对应的(大雾)

Tmac-1 commented 5 years ago

function foo() { console.log(value); } function bar() { value = 2; foo(); } var value = 1; bar(); 这个就会打印2了,这个的执行顺序是什么样的啊?

ddzy commented 5 years ago

function foo() { console.log(value); } function bar() { value = 2; foo(); } var value = 1; bar(); 这个就会打印2了,这个的执行顺序是什么样的啊?

fengandzhy commented 5 years ago

如果是这样的,function out(function(){console.log(xxx)})这种形式,那括号里的函数作用域是怎么样的? 这种写法是错误的。声明函数只能是function out(a){},

rongda commented 5 years ago

JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了,函数的作用域是基于函数创建的位置

chinawzc commented 5 years ago

"根据书写的位置” 先记住这句话 🤣

wyj1993 commented 5 years ago

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

能具体说一下吗,这两个分着看还行,但是合起来就不理解了。

cell617 commented 5 years ago

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

作用域链存储在什么地方

7neves commented 5 years ago

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

function bar() { var value = 2; foo(); } var value = 1; bar(); 为什么这个也会打印1啊。

因为词法作用域,在书写foo()函数的时候,全局变量var value = 1;已经存在了,foo()持有对value的引用,所以不论在哪里执行foo()函数都会输出全局变量中的value的值;第二次写的代码中bar()函数中未用var声明,所以定义了全局变量value = 2;修改了全局变量value的值,所以输出的结果为2;

Wolfeather commented 4 years ago

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

xiaodun commented 4 years ago

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

是用来出面试题嘛