你不知道的Javascript动态做用域

前言

最近被一道面试题给难住了,其实就是说不清楚为何是这个答案,有时候可能屏幕前的你,也会有这个疑惑,因此打算来补一补基础-做用域。javascript

为了更好的理解它,能够看看往期的知识点:java

JavaScript执行上下文-执行栈git

先上题目:github

var Fn = function () {
        console.log(Fn);
    }
    Fn();

    var obj = {
        fn2 : function () {
            console.log(fn2);
        }
    }
    obj.fn2();
复制代码

个人答案认为两个都是打印Function,其实基础扎实的小伙伴估计明白我错哪了。面试

话很少说,开始咱们的正题吧🤭安全

什么是做用域

  1. 任何语言都有做用域的概念,那有些语言做用域是动态的,有些语言做用域是静态的,我我的理解JavaScript做用域是静态的,为何这么说,下面我会说明白的。
  2. 做用域能够理解成:定义了一组明确的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量。

那么,就有人问了,做用域规则在哪里,如何被设置呢?性能优化

官方给出解释:点这里闭包

那么我在这里就不咬文嚼字了,那么咱们要探究的就是静态的问题了🤭函数

静态做用域与动态做用域

由于 JavaScript 采用的是词法做用域,函数的做用域在函数定义的时候就决定了。工具

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

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

var x = 10;
    function fn() {
        console.log(x);
    }
    fn()
    function show(fun) {
        var x = 20;
        fun()
    }
    show(fn);
复制代码

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

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

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

执行 fn函数,依然是从 fn 函数内部查找是否有局部变量 x。若是没有,就从调用函数的做用域,也就是 show函数内部查找 x变量,因此结果会打印 2。

实际JavaScript打印的结果就是1,从结果上说明JavaScript是静态做用域。

为了更好的理解,经过画一张简单图来理解静态做用域:

这样子就很好理解这个关系了,Fn函数在本身的做用域中找变量x,根据变量查找规则,若是没有的话,会去上一级的做用域查找,也就是全局做用域,看是否存在变量x,有的话就取这个值,没有的话就返回undefined。

**一旦找到第一个匹配,做用域查询就中止了。**相同的标识符名称能够在嵌套做用域的多个层中被指定,这称为“遮蔽(shadowing)”(内部的标识符“遮蔽”了外部的标识符)。

上述这个查询的过程,叫作做用域查询,它老是从当前被执行的最内侧的做用域开始,向外/向上不断查找,直到第一个匹配才中止。

做用域分类

全局做用域

在代码任何地方都能访问到的对象拥有全局做用域,更深刻的了解能够结合全局执行上下文。好比: JavaScript的全局对象 函数 变量都能在全局访问到。

3种情形会拥有全局做用域

最外层函数以及最外层定义的变量属于全局做用域
var demo = 1;            //全局变量
        let fn = () => {
            alert(demo)
            let inner = () => alert(demo); 
        }
        fn();  //1 
        inner()  //ReferenceError
复制代码
在任何位置不使用var声明的变量属于全局做用域
var demo = 1;            //全局变量
        let fn = () => {
            demo1 = '未使用var定义'
            alert(demo)
            let inner = () => alert(demo1);
        }
        fn();  //1 
        console.log(window.demo1);   //未使用var定义
复制代码
全部window对象的属性属于全局做用域

局部做用域/函数做用域

和全局做用域相反,函数做用域通常只在函数的代码片断内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数做用域中,其生命周期随着函数的执行结束而结束。例如:

let name = '李四'
        let  getName = () => {
            var name = '张三';
            alert(name); //张三
        }
        console.log(name); //李四
复制代码

块级做用域

在ES6中提出块级做用域概念,它的用途就是:变量的声明应该距离使用的地方越近越好。并最大限度的本地化。避免污染。

块做用域由 { } 包括,let const能够造成块级做用域,也就是俗称的暂时性死区。具体的在这里就不详细的介绍了,感兴趣的能够了解下以前的文章-JavaScript执行上下文-执行栈 这里面讲了为何let const 会存在暂时性死区,原理是什么?

动态做用域

与词法做用域不一样于在定义时肯定,动态做用域在执行时肯定,其生存周期到代码片断执行为止。动态变量存在于动态做用域中,任何给定的绑定的值,在肯定调用其函数以前,都是不可知的。

从某种程度上来讲,这会修改做用域,(也就是欺骗)词法做用域。在你的代码中建议不要使用它们,这是由于在某些方面: 欺骗词法做用域会致使更低下的性能。

eval

JavaScript中的eval(..)函数接收一个字符串做为参数值,并将这个字符串的内容看做是好像它已经被实际编写在程序的那个位置上。

eval(..)被执行的后续代码行中,引擎 将不会“知道”或“关心”前面的代码是被动态翻译的,并且所以修改了词法做用域环境。引擎 将会像它一直作的那样,简单地进行词法做用域查询。

考虑下面代码:

var b = 2;
        function demo(str, a) {
            eval(str);           // 欺骗词法做用域
            console.log(a, b);
        }
        demo("var b = 12;", 1); // 1, 12
复制代码

eval(..)调用的位置上,字符串"var b = 12"被看做是一直就存在第2行的代码。由于这个代码恰巧声明了一个新的变量b,它就修改了现存的demo(..)的词法做用域。事实上,就像上面提到的那样,这个代码实际上在demo(..)内部建立了变量b,它遮蔽了声明在外部(全局)做用域中的b

console.log(..)调用发生时,它会在demo(..)的做用域中找到ab,并且毫不会找到外部的b。这样,咱们就打印出"1, 12"而不是通常状况下的"1, 2"。

假设:eval(..)执行的代码字符串包含一个或多个声明(变量或函数)的话,这个动做就会修改这个eval(..)所在的词法做用域。技术上讲,eval(..)能够经过种种技巧(超出了咱们这里的讨论范围)被“间接”调用,而使它在全局做用域的上下文中执行,如此修改全局做用域。但不论那种状况,eval(..)均可以在运行时修改一个编写时的词法做用域。

注意:eval(..)被用于一个操做它本身的词法做用域的strict模式程序时,在eval(..)内部作出的声明不会实际上修改包围它的做用域。

var b = 2;
        function demo(str, a) {
 'use strict'
            eval(str);      // 欺骗词法做用域不生效
            console.log(a, b);
        }
        demo("var b = 12;", 1); // 1, 2
复制代码

在JavaScript中还有其余的工具拥有与eval(..)很是相似的效果。setTimeout(..)setInterval(..)能够 为它们各自的第一个参数值接收一个字符串,其内容将会被eval为一个动态生成的函数的代码。这种老旧的,遗产行为早就被废弃了。别这么作!

new Function(..)函数构造器相似地为它的 最后 一个参数值接收一个代码字符串,来把它转换为一个动态生成的函数(前面的参数值,若是有的话,将做为新函数的命名参数)。这种函数构造器语法要比eval(..)稍稍安全一些,但在你的代码中它仍然应当被避免。

在你的代码中动态生成代码的用例少的难以想象,由于在性能上的倒退使得这种能力几乎老是得不偿失。

with

MDN最新规范不建议使用,因此接下来咱们了解下with语句就行。

with语句接收一个对象,这个对象有0个或多个属性,并 将这个对象视为好像它是一个彻底隔离的词法做用域,所以这个对象的属性被视为在这个“做用域”中词法定义的标识符。

function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b: 3
};

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 哦,全局做用域被泄漏了!
复制代码

在这个代码示例中,建立了两个对象o1o2。一个有a属性,而另外一个没有。foo(..)函数接收一个对象引用obj做为参数值,并在这个引用上调用with (obj) {..}。在with块儿内部,咱们制造了一个变量a的看似是普通词法引用的东西,并将值2赋予它。

当咱们传入o1时,赋值a = 2找到属性o1.a并赋予它值2,正如在后续的console.log(o1.a)语句反应的那样。然而,当咱们传入o2,由于它没有a属性,没有这样的属性被建立,因此o2.a仍是undefined

可是以后咱们注意到一个特别的反作用,赋值a = 2建立了一个全局变量a。这怎么可能?

注意: 尽管一个with块儿将一个对象视为一个词法做用域,可是在with块儿内部的一个普通var声明将不会归于这个with块儿的做用域,而是归于包含它的函数做用域。

with语句其实是从你传递给它的对象中凭空制造了一个 全新的词法做用域

以这种方式理解的话,当咱们传入o1with语句声明的“做用域”就是o1,并且这个“做用域”拥有一个对应于o1.a属性的“标识符”。但当咱们使用o2做为“做用域”时,它里面没有这样的a“标识符”,因而就会出现undefined

“做用域”o2中没有,foo(..)的做用域中也没有,甚至连全局做做用域中都没有找到标识符a,因此当a = 2被执行时,其结果就是自动全局变量被建立(由于咱们没有在strict模式下)。

with在运行时将一个对象和它的属性转换为一个带有“标识符”的“做用域”,这个奇怪想法有些烧脑。可是对于咱们看到的结果来讲,这是我能给出的最清晰的解释。

动态做用域性能

经过在运行时修改,或建立新的词法做用域,eval(..)with均可以欺骗编写时定义的词法做用域。

JavaScript 引擎 在编译阶段期行许多性能优化工做。其中的一些优化原理都归结为实质上在进行词法分析时能够静态地分析代码,并提早决定全部的变量和函数声明都在什么位置,这样在执行期间就能够少花些力气来解析标识符。

但若是 引擎 在代码中找到一个eval(..)with,它实质上就不得不 假定 本身知道的全部的标识符的位置多是不合法的,由于它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法做用域,或者你可能会向with传递的对象有什么样的内容来建立一个新的将被查询的词法做用域。

换句话说,悲观地看,若是eval(..)with出现,那么它 作的几乎全部的优化都会变得没有意义,因此它就会简单地根本不作任何优化。

你的代码几乎确定会趋于运行的更慢,只由于你在代码的任何地方引入了一个了eval(..)with。不管 引擎 将在努力限制这些悲观臆测的反作用上表现得多么聪明,都没有任何办法能够绕过这个事实:没有优化,代码就运行的更慢。

结论

  • 做用域是一组规则,它决定了一个变量(标识符)在哪里和如何被查找。
  • 做用域是由编写时函数被声明的位置的决策定义的,并非说函数在哪里执行,哪里就开始生成做用域,这点理解很重要,这也时区分静态做用域和动态做用域区别的一个方法。
  • 查找一个变量时,都从当前执行中的 做用域 开始,若是有须要(也就是,它们在这里没能找到它们要找的东西),它们会在嵌套的 做用域 中一路向上,一次一个做用域(层)地查找这个标识符,直到它们到达全局做用域(顶层)并中止,既可能找到也可能没找到。
  • eval(…) 和 with 均可以 '欺骗' 词法做用域,前者能够经过对一个拥有一个或多个声明的“代码”字符串进行求值,来(在运行时)修改现存的词法做用域。后者实质上是经过将一个对象引用看做一个“做用域”,并将这个对象的属性看做做用域中的标识符,(一样,也是在运行时)建立一个全新的词法做用域。
  • 以上两种机制的缺点也很明显,它们压制了引擎在做用域查询上进行编译期优化的能力,由于引擎不得不悲观的假定这样子的优化不合理,这两种机制会使代码运行的更慢!!! 建议不使用它们

参考

你不懂JS:做用域与闭包

JS做用域

官方中文版原文连接-推荐看这个

做用域气泡

JavaScript深刻之词法做用域和动态做用域

相关文章
相关标签/搜索