美文网首页WEB前端程序开发Web前端之路前端开发部落
想成为JS大牛, 作用域是你必须知道的

想成为JS大牛, 作用域是你必须知道的

作者: 叶子编程 | 来源:发表于2018-04-04 01:09 被阅读51次

    我们常说,万物都有其存在的价值,这话的确不错,但是深思一下,是不是需要有个前提,万物都在某些领域或多或少的存在某些价值。

    举个例子,汽车,绝对是个非常有价值的stuff,它给我们的日常出行,货物运输等带来了极大的便利;筷子,同样也是个非常有价值的stuff,它给我们吃饭带来了极大的方便。但是,汽车能帮我们把菜送到嘴里吗?筷子能载着我们出行吗?

    那么,我上面所说的某些领域,我们是不是可以称其为作用域,我想是可以的。

    说到这,那么我就想问了:在JS里,作用域是不是也是类似的概念呢?

    首先,我可以肯定的说这是一个在JavaScript中灰常灰常重要的概念,关系着JS里很多核心的机制,理解它,很多问题都迎刃而解了。

    那么,问问自己,在JS里,作用域是什么?

    心里大概知道是什么,但是细细一想又好像说不太清。

    没关系,下面我们就细细品味这个有意思的东东。

    先throw概念吧:

    作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

    通俗来说,作用域相当于一个管理员(有自己的一套规则),他负责管理所有声明的标识符的有序查询。

    我们来讲个故事,说说作用域到底干了啥。

    三兄弟齐上阵

    long long ago,有3个关系很好的基友,老大叫引擎,老二叫编辑器,老三叫作用域。三兄弟眼看年岁已长,可手上还是没有几个银子。个个都很着急,于是三兄弟谋划一同做个事。

    求职过程:此粗略去数万个字。。。

    最终他们做的工作是:负责JS的编译和运行。

    他们的工作内容是这样的:

    老板甩给他们一项任务编译并执行下面代码:

    var a = 1;

    console.log( a );

    开始工作:

    编译器:作用域,帮我看看你那有没有储存变量a。

    作用域:二哥,还没有。

    编译器:那好,帮我储存一个。

    引擎: 老三,你那有没有一个叫做a的变量。

    编译器:大哥,还真有,刚二哥让我存储了一个。

    引擎: 真是太好了,帮我拿出来,它的值是几,我需要给它复制。

    编译器:大哥,它的值是2。

    引擎: 谢谢你,三弟,这样我就能打印它的值了。

    上面讲了一个不恰当的小故事,但是三者之间的关系大概就是这样。

    词法作用域 VS 动态作用域

    词法作用域

    彻底搞懂JavaScript作用域里介绍过,大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。回忆一下,词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

    在JS里,使用的作用域就是词法作用域。

    简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

    动态作用域

    在JS里,动态作用域和this机制息息相关。它的作用域诗是在运行的过程中确定的

    var a = 1;

    function foo() {

    var a = 2;

    console.log( this.a );

    }

    foo(); // 1

    从上面的代码,我们可以看出:foo中打印a的值不是由写代码的位置确定的,而是取决于foo执行的位置。

    区别

    词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)

    词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

    函数作用域

    JS里,生成作用域的方式:

    函数

    with、eval (不建议使用,影响性能)

    由此,我们知道JS里,绝大多数的作用域都是基于函数生成的。

    每个函数都会为自身生成一个作用域气泡。这个气泡内所有的标识符都可以在这个气泡中使用。

    function bar() {

    var a = 1;

    function foo() {

    var b = 2;

    console.log(b);

    }

    foo();

    console.log(a);

    }

    bar();

    上面代码,bar气泡有标识符a、foo,因此在bar气泡中可以访问到a、foo; foo气泡有标识符b,因此在bar气泡中可以访问到b; 当然还有一个全局气泡,全局气泡中有bar标识符,因此在全局气泡中可以访问到bar。

    最小授权原则

    最小授权原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

    这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作 用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏前面提到的最小 特权原则,因为可能会暴漏过多的变量或函数,而这些变量或函数本应该是私有的,正确 的代码应该是可以阻止对这些变量或函数进行访问的。

    例如:

    function doSomething(a) {

    b = a + doSomethingElse( a * 2 );

    console.log( b * 3 );

    }

    function doSomethingElse(a) {

    return a - 1;

    }

    var b;

    doSomething( 2 ); // 15

    在这个代码片段中,变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 内部具体 实现的“私有”内容。给予外部作用域对 b 和 doSomethingElse(..) 的“访问权限”不仅 没有必要,而且可能是“危险”的,因为它们可能被有意或无意地以非预期的方式使用, 从而导致超出了 doSomething(..) 的适用条件。更“合理”的设计会将这些私有的具体内容隐藏在 doSomething(..) 内部,

    例如:

    function doSomething(a) {

    function doSomethingElse(a) {

    return a - 1;

    }

    var b;

    b = a + doSomethingElse( a * 2 );

    console.log( b * 3 );

    }

    doSomething( 2 ); // 15

    现在,b 和 doSomethingElse(..) 都无法从外部被访问,而只能被 doSomething(..) 所控制。 功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会 依此进行实现。

    规避冲突

    当我们的程序代码逐渐多起来,难免会出现变量冲突。那么如何规避冲突就显得额外重要。

    函数可以把标识符严谨的"隐藏"起来,外部无法访问到,利用这个特性我们可以很好的规避冲突。

    function foo() {

    var a = 1;

    }

    function bar() {

    var a = 2;

    }

    foo和bar中定义了相同的变量a,但是却不会相互造成影响。因为函数可以很好的把标识符"隐藏"起来。

    全局命名空间

    变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它 们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。 这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象 被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属 性,而不是将自己的标识符暴漏在顶级的词法作用域中。

    例如:

    var myLibrary = {

    name: 'echo',

    getName: function() {

    console.log( this.name );

    }

    }

    函数声明 VS 函数表达式

    函数声明和函数表达式判别的依据是:函数的生命是否以function关键词开始。以关键词function 开始的声明是函数声明,其余的函数声明全部是函数表达式。

    //函数声明

    function foo() {

    }

    //函数表达式

    var foo = function () {

    };

    (function() {

    })();

    具名函数 VS 匿名函数

    具名函数 拥有名字的函数

    function foo() {

    }

    var foo = function bar() {

    }

    setTimeout( function foo() {

    } )

    +function foo() {

    }();

    需要注意:函数声明一定要是具名函数。

    匿名函数 没有名字的函数

    var foo = function () {

    }

    setTimeout( function foo() {

    } )

    -function foo() {

    }();

    立即执行函数(IIFE)

    vara=2;

    (function foo() {

    var a=3;

    console.log( a ); // 3

    })();

    console.log( a ); // 2

    该函数是以()开始,不是以关键词function开始,因此IIFE是函数表达式

    函数名对 IIFE 当然不是必须的,IIFE 最常见的用法是使用一个匿名函数表达式。虽然使 用具名函数的 IIFE 并不常见,但它具有以下优势:

    匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。

    如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑 自身。

    匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让 代码不言自明。

    因此具名函数的 IIFE 也是一个值得推广的实践。

    另一种表达形式

    (function() {

    }())

    这也是IIFE的一种表达方式,功能上和上面那种方式是一致的。选择哪种全凭个人爱好。

    参数传递

    IIFE 也可以和其他形式的函数一样实现参数的传递(多说一句:参数传递是按值传递)。

    (function foo(a) {

    console.log(a);

    })(3);

    这个模式的另外一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常(虽 然不常见)。将一个参数命名为 undefined,但是在对应的位置不传入任何值,这样就可以 保证在代码块中 undefined 标识符的值真的是 undefined:

    undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!

    (function IIFE( undefined ) {

    var a;

    if (a === undefined) {

    console.log( "Undefined is safe here!" );

    }

    })();

    UMD (Universal Module Definition)

    IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。尽管这种模式略显冗长,但有些人认为它更易理解。

    var a=2;

    (function IIFE( def ) {

    //参数的处理

    def( window );

    })(function def( global ) {

    //逻辑运算

    var a=3;

    console.log( a ); // 3

    console.log( global.a ); // 2

    });

    块作用域

    尽管函数作用域是最常见的作用域单元,当然也是现行大多数 JavaScript 中最普遍的设计 方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可 以实现维护起来更加优秀、简洁的代码。

    try...catch非常少有人会注意到 JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域, catch 的参数变量仅在 catch 内部有效。

    try{

    throw undefined;

    }catch(a){

    a = 2;

    console.log(a); // 2

    }

    console.log(a);  // ReferenceError

    let

    ES6的标准使我们能够简单的创建块作用域,其中一个变量定义方式是let关键词定义。

    let定义的变量具有以下的特点:

    let隐形的创建块作用域({...})

    let声明的变量不能进行变量提升,因此只能先定义,后使用

    {

    let a = 1;

    console.log(a); // 1

    }

    console.log(a);  // ReferenceError

    let一个典型的应用就是在for循环里

    我们看下面两个例子:

    // 每秒输出一个5

    for( var i = 0; i < 5 ; i++ ) {

    setTimeout(() => {

    console.log( i );

    }, i *1000)

    }

    // 依次输出0,1,2,3,4,时间间隔位1秒

    for( let i = 0; i < 5 ; i++ ) {

    setTimeout(() => {

    console.log( i );

    }, i *1000)

    }

    其原因就是let形成了5个块作用域,使每次输出的变量都从本次循环的块作用域中获取。

    当然我们还可以有其他方式做到第二种效果,我们将在闭包,是真的美中说道。

    const

    除了 let 以外,ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的 (常量)。之后任何试图修改值的操作都会引起错误。

    var foo = true;

    if (foo) {

    var a=2;

    const b = 3; // 包含在 if 中的块作用域常量

    a=3;//正常!

    b=4;//错误!

    }

    console.log( a ); // 3

    console.log( b ); // ReferenceError!

    作用域链

    作用域链是由当前作用域与上层一系列父级作用域组成,作用域的头部永远是当前作用域,尾部永远是全局作用域。作用域链保证了当前上下文对其有权访问的变量的有序访问。

    var a = 2;

    function bar() {

    function foo() {

    console.log(a);

    }

    }

    bar(); // 2

    上面代码是由3层作用域气泡组成,foo气泡中试图打印变量a,引擎在foo气泡中未找到a变量,于是去其父作用域气泡bar中寻找...以此类推直到找到全局作用域气泡,发现有变量a,将其值打印出来。如若没找到,报ReferenceError错误。

    相关文章

      网友评论

      • IT人故事会:贵在坚持,么么哒!我也是个爱写文章的人

      本文标题:想成为JS大牛, 作用域是你必须知道的

      本文链接:https://www.haomeiwen.com/subject/rsuhhftx.html