《你不知道的JavaScript》:函数作用域和块作用域

《你不知道的JavaScript》第一部分作用域和闭包第2篇。

昨天讲到作用域,回顾下概念:作用域是一套用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找的规则。

常见的作用域形式

在JS中,最常见的作用域是函数作用域,其他结构通常不会创建作用域。但随着js的迭代,现在也有了块作用域,将在后面讲到。

而函数作用域的含义是指:属于这个函数的全部变量都可以在整个函数的范围内使用及复用,包括在嵌套的函数作用域中也可以使用。

函数作用域的常规套路是,先声明一个函数,然后向函数中添加代码实现。

但这个套路反过来也是很有用,即选取所写的一部分代码用函数声明来包装,从而将这些代码中的所有变量都绑定在新创建的包装函数的作用域中,而非先前所在的作用域中,目的就是通过新建作用域隐藏这些变量,能够尽可能少的暴露变量,这是符合软件开发的最小特权原则的。除此以外,"隐藏"作用域中的变量和函数还能规避同名标识符之间可能存在的冲突问题。

函数作用域的创建方式

函数作用域的创建需要声明一个函数,而声明函数这个行为又有函数声明和函数表达式两种操作方式。

函数声明和函数表达式的辨别,可以通过一个小技巧来一眼分辨:看function关键字出现在声明中的位置,注意,不仅仅是一行代码,而是整个声明中的位置,如果function是声明中的第一个词,那就是函数声明,否则就是函数表达式。

function foo(){};   //函数声明

var foo = function(){};     //函数表达式

(function foo(){});     //函数表达式

(function(){       //匿名函数表达式

});

函数声明和函数表达式的区别是它们的名称标识符将会绑定在何处。举个例子:

var a = 10;
function foo(a){
    var b = a * 2;
    return b;
}
console.log(foo);       //正确打印foo函数
foo();

(function fn(a){
    var c = a + 10;
    console.log(fn);    //正确打印fn函数
    return c;
})();
console.log(fn);    //ReferenceError: fn is not defined

上例中,假设代码所处作用域为全局作用域,foo函数的访问作用域是全局作用域,fn函数的访问作用域被绑定在函数表达式自身的函数中而非所在的全局作用域。此时,fn变量被隐藏在自身作用域中就意味着不会非必要的污染外部作用域。

在前文的函数表达式举例中,我还列出了匿名函数表达式,这种函数表达式的常用之地是回调函数,它是没有名称标识符的。函数表达式可以省略函数名,但函数声明则不可以省略函数名,否则会报错。

匿名函数表达式的应用非常常见,很多工具或库都有用到,但其也存在几个缺点:

  • 匿名函数在栈中不显示有意义函数名,调试困难;
  • 由于没有函数名,所以如需调用自身,比如递归或者事件触发后事件监听器需要解绑自身等,就不太好办了,除非使用arguments.callee,但这个已非官方推荐实践,将被彻底废弃;
  • 同样由于没有函数名,导致代码可读性差,毕竟有个好的描述性名称,胜过额外添加注释。

正是由于以上三个缺点,所以比较推荐为匿名函数表达式加了名称标识符,这个操作不会对代码实现有任何影响,还能一举解决上面三个缺点,何乐不为:

setTimeout(function foo(){
    console.log("哈哈,我有名称了.")
}, 1000)
//1秒后打印:
//哈哈,我有名称了.

块作用域

在ES5及之前版本中,js中的块作用域形同于无,实在要说的话,也只有try-catch中的catch部分定义的变量所在作用域是catch块中的,其他的都只是样子像,而本质上都不是块作用域,例如

for(var i=0; i<10; i++){
    console.log(i);
}
console.log('外部:'+i);
// 外部:10

上例外部作用域可以访问到i的值为10

但在ES6版本开始,有了letconst,终于可以明目张胆的定义块级作用域了,想必用惯了其他语言块级作用域的同学,心里的别扭终于可以舒口气了吧。

let关键字可以将变量绑定到所在的任意作用域中,通常是{...}内部,也就是说,let关键字为其声明的变量隐式的定义了所在的块级作用域。

let关键字发挥作用的典型在于for循环。

for(let i=0; i<10; i++){
    console.log(i);
}
console.log(i);     //ReferenceError:i is not defined

你看,在外部作用域访问变量标识符i时,就直接报未定义的语法错误。

事实上,for循环头部的let不仅将i绑定到for循环的块中,在每次循环开始时,还将其重新绑定到新的循环迭代中去,确保使用上一个循环迭代结束时的值重新赋值。

至于const也是可以创建块作用域中,不同于let的是,其值是固定的常量,任何对其值的修改都会引起错误。

总结一下

js中的作用域,主要有函数作用域和块级作用域,当然还有全局作用域。

函数作用域的使用,可以隐藏代码实现,减少变量暴露,避免命名冲突,符合软件设计的最小特权原则。关于函数作用域,还讲了函数声明与函数表达式的辨别方法和区别。在函数表达式中,还分出了命名函数表达式和匿名函数表达式。

块级作用域的实现,有赖于ES6的版本进步,提供letconst关键字,可以实现同其他语言相同的由{...}包裹起来的块级作用域。比较典型的就是let版的for循环和var版的for循环,感兴趣的可以自行了解。

-------------------------------- 热门文章 --------------------------------

本文分享自微信公众号 - 前端小二(frontendxiao2)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我的前端路

异步流程控制:7 行代码学会 co 模块

首先请原谅我的标题党(●—●),tj 大神的 co 模块源码200多行,显然不是我等屌丝能随便几行代码就能重写的。只是当今大家都喜欢《7天学会xx语言》之类的速...

9700
来自专栏Super 前端

深入理解ES6--迭代器、生成器、代理、反射、Promise

注意:数组和Set集合的默认迭代器是values();Map集合的默认迭代器是entries()

8620
来自专栏Super 前端

现代前端技术解析:前端三层结构与应用

前端三个基本结构:结构层HTML、表现层CSS、行为层JavaScript。现在的Web前端应用已经不是简单的三层结构就能轻松解决,而是已经形成了编译流程化、生...

6920
来自专栏Super 前端

深入理解ES6--解构

通过在数组中的位置进行选取,且可以将其存储在任意变量中,未“显式声明”的元素都会被直接被忽略。

9320
来自专栏我的前端路

ES6 你可能不知道的事 – 基础篇

ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生。

9500
来自专栏Super 前端

深入理解ES6--Set、Map及Symbol

由于对象属性名必须是字符串,所以5会转换为字符串”5”;而key1和key2会转换为["object Object"]。对于,Set集合和Map集合是严格区分的...

7820
来自专栏女程序员的日常_Lin

Iterator 、Generator

JS里原有的表示”集合“的数据结构,主要是Array和Object,ES6又添加了Map和Set。我们可以任意组合和设计数据的结构,那么就需要一个机制,可处理所...

9320
来自专栏Super 前端

深入理解ES6--块级作用域、字符串、正则、数组

在for-in和for-of循环中,因为每次迭代不会(像for循环的例子一样)修改已有绑定,而是会创建一个新绑定。

7120
来自专栏Super 前端

深入理解ES6--对象、函数扩展

javascript引擎会在访问作用域中查找其同名变量;如果找到,则变量的值被赋给对象字面量的同名属性。

7630
来自专栏全栈者

[JavaScript进阶]从JavaScript原型到面向对象

首先给出结论,JavaScript 的本身是支持面向对象的,它本身具备着强大灵活的 OOP 语言能力。但是对于使用过基于类的语言 (如 Java 或 C++) ...

11010

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励