JavaScript开发者应懂的33个概念<7>-函数作用域, 块级作用域和词法作用域
目录
- 调用堆栈
- 原始类型
- 值类型和引用类型
- 隐式, 显式, 名义和鸭子类型
- == 与 ===, typeof 与 instanceof
- this, call, apply 和 bind
- 函数作用域, 块级作用域和词法作用域
- 闭包
- map, reduce, filter 等高阶函数
- 表达式和语句
- 变量提升
- Promise async 与 wait
- 立即执行函数, 模块化, 命名空间
- 递归
- 算法
- 数据结构
- 消息队列和事件循环
- setTimeout, setInterval 和 requestAnimationFrame
- 继承, 多态和代码复用
- 按位操作符, 类数组对象和类型化数组
- DOM 树和渲染过程
- new 与构造函数, instanceof 与实例
- 原型继承与原型链
- Object.create 和 Object.assign
- 工厂函数和类
- 设计模式
- Memoization
- 纯函数, 函数副作用和状态变化
- 耗性能操作和时间复杂度
- JavaScript 引擎
- 二进制, 十进制, 十六进制, 科学记数法
- 偏函数, 柯里化, Compose 和 Pipe
- 代码整洁之道
简介
记录一个重新学习javascript的过程 ,文章并不是按顺序写的,写完就会更新目录链接 本篇文章目录是参照 @leonardomso 创立,英文版项目地址在这里
1. 函数作用域
ES5种只存在两种作用域
- 函数作用域
- 全局作用域
let a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a); //3
}
}
上面的例子中可以看出
- f1的作用域指向有全局作用域(window) 和它本身,
- 而f2的作用域指向全局作用域(window)、 f1和它本身。
- 而且作用域是从最底层向上找, 直到找到全局作用域window为止,
- 如果全局还没有的话就会报错。闭包产生的本质就是,
- 当前环境中存在指向父级作用域的引用。
function f2() {
var a = 2
function f3() {
console.log(a); //2
}
return f3;
}
var x = f2();
x();
这里x会拿到父级作用域中的变量, 输出2。
因为在当前环境中,含有对f3的引用, f3恰恰引用了window、 f3和f3的作用域。 因此f3可以访问到f2的作用域的变量。
var f4;
function f5() {
var a = 2
f4 = function () {
console.log(a);//2
}
}
f5();
f4();
让f5执行,给f4赋值后,等于说现在f4拥有了window、f5和f4本身这几个作用域的访问权,还是自底向上查找,最近是在f5中找到了a,因此输出2。在这里是外面的变量f4存在着父级作用域的引用,
经典的一道题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
} // 6 6 6 6 6 6
// 为什么会全部输出6? 如何改进, 让它输出1, 2, 3, 4, 5?
- 因为setTimeout为宏任务, 由于JS中单线程eventLoop机制, 在主线程同步任务执行完后才去执行宏任务。
- 因此循环结束后setTimeout中的回调才依次执行, 但输出i的时候当前作用域没有。
- 往上一级再找,发现了i,此时循环已经结束,i变成了6,因此会全部输出6。
用es5 解决
利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j)//0,1,2,3,4
}, 1000);
})(i)
}
如何解决呢?看下面的块级作用域
2. 块级作用域
使用ES6中的let
- let使JS发生革命性的变化, 让JS有函数作用域变为了块级作用域,
- 用let后作用域链不复存在。 代码的作用域以块级为单位
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)//1,2,3,4,5
}, 2000)
}
什么是块级作用域
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域,例如for、while、if
JavaScript如何实现块级作用域
虽然js中没有块级作用域,但是可以用闭包/匿名函数来模仿块级作用域。
块级作用域(通常称为私有作用域)的匿名函数的语法如下:
(function () {
//这里是块级作用域
})();
3.词法作用域
词法作用域是由你的代码中将变量和块作用域写在哪里来决定的。比如下面的if,let 在{}中定义了一个变量a,那么a的词法作用域就在if的块区域之中,也就是出生在哪他的词法作用域就在哪
if(1){
let a = 1
}
console.log(a);
那么上面那段代码究竟会输出什么呢? 答案是由于在全局中找不到a的声明,所以将会抛出一个报错。当然神奇的是如果你将let换成var,他将会输出1,原因就是由于变量提升事实上var的声明跳出了if,那么它的作用域就是全局。
欺骗词法作用域
1.eval
let fn1 = function(str){
eval(str)
console.log(a);//3
}
var a = 1
fn1('var a = 3')
eval(..)函数的作用其实就可以理解为将其中的内容搬到调用这个函数的时候去用,也就是说此时运行代码你会得到3。而按照正常的理解来说内部没有找到var声明就需要向外寻找那么会找到1,而且因为词法作用域问题eval函数中的声明也不应该在上一级的input中生效可是神奇的是这个方法让a成功被声明了,说明eval函数中声明的变量的词法作用域被改变了!
2.with
function foo(obj){
with(obj){
c = 1
}
}
let obj = {
a : 1
}
foo(obj)
console.log(c);//1
我们利用with对一个obj中的c进行了改变,但事实上obj中并没有c的存在,那么最后我们打印c的时候应该是报错的,因为我们并没有定义一个c对吧?可是你可以尝试运行一下这段代码,你会惊奇的发现:输出了1!没错,c被输出了,可是无论怎么看c的作用域都不可能是全局的,可log在全局只能输出作用域在全局变量。那么这到底是为什么呢?
其实这被称为“泄露”,with将c泄露到了全局,因为它并没有在obj中找到c,结果它就非常离谱的在全局凭空创造了一个变量c并且给它赋了值。
相比于eval只是给声明“搬了个家”,with直接“建了个家”从而欺骗词法作用域更加离谱,它们二者都会拖慢代码的运行,除去一些特殊情况我们可以尽量不要使用这两种机制。