JavaScript开发者应懂的33个概念<7>-函数作用域, 块级作用域和词法作用域

JavaScript开发者应懂的33个概念<7>-函数作用域, 块级作用域和词法作用域

目录

  1. 调用堆栈
  2. 原始类型
  3. 值类型和引用类型
  4. 隐式, 显式, 名义和鸭子类型
  5. == 与 ===, typeof 与 instanceof
  6. this, call, apply 和 bind
  7. 函数作用域, 块级作用域和词法作用域
  8. 闭包
  9. map, reduce, filter 等高阶函数
  10. 表达式和语句
  11. 变量提升
  12. Promise async 与 wait
  13. 立即执行函数, 模块化, 命名空间
  14. 递归
  15. 算法
  16. 数据结构
  17. 消息队列和事件循环
  18. setTimeout, setInterval 和 requestAnimationFrame
  19. 继承, 多态和代码复用
  20. 按位操作符, 类数组对象和类型化数组
  21. DOM 树和渲染过程
  22. new 与构造函数, instanceof 与实例
  23. 原型继承与原型链
  24. Object.create 和 Object.assign
  25. 工厂函数和类
  26. 设计模式
  27. Memoization
  28. 纯函数, 函数副作用和状态变化
  29. 耗性能操作和时间复杂度
  30. JavaScript 引擎
  31. 二进制, 十进制, 十六进制, 科学记数法
  32. 偏函数, 柯里化, Compose 和 Pipe
  33. 代码整洁之道

简介

记录一个重新学习javascript的过程 ,文章并不是按顺序写的,写完就会更新目录链接 本篇文章目录是参照 @leonardomso 创立,英文版项目地址在这里

1. 函数作用域

ES5种只存在两种作用域

  1. 函数作用域
  2. 全局作用域
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直接“建了个家”从而欺骗词法作用域更加离谱,它们二者都会拖慢代码的运行,除去一些特殊情况我们可以尽量不要使用这两种机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值