• 前端JS基础第二篇:作用域与作用域链


    目录

    作用域与作用域链

    什么是作用域

    作用域类型

    var与let的经典案例

    let实现原理

    作用域链


    作用域与作用域链

    什么是作用域

    作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性。
    从使用方面来解释,作用域就是变量的使用范围,也就是在代码的哪些部分可以访问这个变量,哪些部分无法访问到这个变量,换句话说就是这个变量在程序的哪些区域可见。代码演示:

    1. function Fun() {
    2. var inVariable = "内部变量";
    3. }
    4. Fun();
    5. console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
    6. //inVariable是在Fun函数内部被定义的,属于局部变量,在外部无法访问,于是会报错

    作用域类型

    全局作用域函数作用域、ES6中新增了块级作用域
    函数作用域是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了
    块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
    在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问

    var与let的经典案例

    1. 用var定义i变量,循环后打印i的值
    1. var a = [];
    2. for (var i = 0; i < 10; i++) {
    3. a[i] = function () {
    4. console.log(i); // i 指向全局的 i,也就是数组中函数所有的i都指向的是同一个变量i
    5. };
    6. }
    7. a[6](); // 10

    上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会自增,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是循环结束之后i的值,也就是 10。

    如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

    1. var a = [];
    2. for (let i = 0; i < 10; i++) {
    3. a[i] = function () {
    4. console.log(i); //由于 let 创建的变量是块级作用域的,所以在这个函数内每次都保存一个新的 i
    5. };
    6. }
    7. a[6](); // 6

    这是因为let创建的变量是块级作用域的,所以每次循环都是一个新的 i,每次的值都是 i++ 的结果。因为每次循环的 i 都是一个独立的变量(内存里的唯一地址),因此闭包记录的值都是唯一的,所以才能得到最终的结果

    如果用 var 的话,变量 i 是一个全局变量,虽然循环体内每次都创建了一个函数来打印 i,但是当时当刻仅仅是一个指向全局变量 i 的指针,当循环结束之后无论你用哪一个下标去访问循环创建的闭包函数,打印的变量 i 都是全局的那一个,所以全部都是 10。

    有的同学可能会感到疑惑,就算i指向全局的那个,为什么全部都是10呢?下面的循环不是正常输出0~9吗:

    1. for (let i = 0; i < 10; i++) {
    2. console.log(i); //0 1 2 3 4 5 6 7 8 9
    3. }

    实际上我们的示例代码相当于:

    1. {
    2. var i=0;
    3. a[0] = function () {
    4. console.log(i)
    5. }
    6. }
    7. {
    8. var i=1;
    9. a[1] = function () {
    10. console.log(i)
    11. }
    12. }
    13. {
    14. var i=2;
    15. a[2] = function () {
    16. console.log(i)
    17. }
    18. }

    再看下面这个例子:

      • <li>1li>
      • <li>2li>
      • <li>3li>
      • <li>4li>
      • <li>5li>
  • <script>
  • // 获取元素
  • var lis = document.querySelectorAll('li');
  • for(var i = 0; ilength; i ++){
  • lis[i].onclick = function(){
  • console.log(i);//5 不能使用i
  • }
  • }
  • script>
  • 当点击事件触发时,for循环已经执行结束了,由于i是var定义的,后面定义的i会覆盖前面的,所以最后i为5,当执行函数时,函数内没有i,根据作用域链向父级找,得到的是全局变量i = 5。聪明的小伙伴肯定可以举一反三,如果函数内不是点击事件是类似setTimeout这类的异步操作,最后的结果也是一个道理。

    let实现原理

    利用 JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征,不难改造出下面的代码:

    1. // 用var实现案例2的效果
    2. var a = [];
    3. var _loop = function _loop(i) {
    4. a[i] = function() {
    5. console.log(i);
    6. };
    7. };
    8. for (var i = 0; i < 10; i++) {
    9. _loop(i);
    10. }
    11. a[0](); // 0

    作用域链

    当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

    看下面这段代码:

    1. var a = 'jack';
    2. var b = 'andy';
    3. function fn() {
    4. var a = 'frank';
    5. console.log(a);
    6. console.log(b);
    7. }
    8. fn();
    9. console.log(a);

    它的输出结果是 frank andy jack

    第二个console.log为什么会输出全局变量andy呢?

    这个时候就有了作用域链的概念了,简单的说作用域表示区域,作用域链表示次序
    现在我们把眼光放在函数fn里,第一行定义了a是局部变量,第二行输出这个a,但是整个代码里定义了两个a,那么就需要刚刚说到的作用域链来决定到底先用哪个变量。
    js会先看函数内有没有这个变量a,如果没有再去函数的外围看有没有这个变量,这里作用域链就帮我们安排好了这个次序。
    所以,函数内定义了变量a为frank,那么第二行就会输出frank,第三行要输出变量b,我们先看函数内有没有这个变量,发现没有,再去外围发现有全局变量b,那么输出的就是这个值,我们再来看最后一console.log(a),因为他在全局范围内,所以只能访问全局变量a。

    也就是说:作用域链只能向上查找,最终找到全局。不能同级(局部)或者向下查找

    我们再看这一段代码:

    1. var a = 'jack';
    2. function fn() {
    3. console.log(a);
    4. var a = 'andy';
    5. console.log(a);
    6. }
    7. fn();

    我们思考一下会输出什么呢?结果是 undefined andy

    稍微有点js经验的同学应该都会答对,因为有变量提升,变量a在第一行就被声明了,只不过没有被赋值。下面修改一下代码,下面再看看会输出什么:

    1. var a = 'jack';
    2. function fn() {
    3. console.log(a);
    4. var a = 'andy';
    5. console.log(ss());
    6. function ss() {
    7. return a;
    8. }
    9. }
    10. fn();

    我们在fn函数内又添加了一个函数ss,并且在这个函数的顶部就调用了这个函数。不仅函数内声明的变量会被提升,函数内的函数也会被提升,而且函数的提升会比变量更优先

    那么,在javascript中这段代码实际上是这样被执行的:

    1. var a = 'jack';
    2. function fn() {
    3. function ss() {
    4. return a;
    5. }
    6. var a;
    7. console.log(a);
    8. a = 'andy';
    9. console.log(ss());
    10. }
    11. fn();

    先把函数声明提升到首行,再声明变量a,然后输出aa没有被赋值,所以是undefined,然后a被赋值为'andy',最后调用函数ss,返回的就是andy

  • 相关阅读:
    讨论 | AR 应用落地前,要做好哪些准备?
    基本算法-希尔排序
    DPDK环境搭建
    提升吃鸡战斗力,分享顶级游戏干货,一站式解决你的游戏需求!
    Linux bash: ipconfig: command not found解决方法
    计算机毕业设计(附源码)python制造型企业仓储管理系统
    监听事件及普通事件的比较
    8.8本周总结
    JavaScript的内置类
    STM: SpatioTemporal and Motion Encoding for Action Recognition 论文阅读
  • 原文地址:https://blog.csdn.net/qq_49900295/article/details/127936828