• JavaScript作用域、作用域链 学习随笔


    (本文是这些知识点的自我理解。写之余从头回顾,加深理解、取得更多收获之用。)

    作用域(scope)

    程序设计概念,通常来说,一段程序代码中所用到的名字(JS叫标识符(如变量名、函数名、属性名、参数..))并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

    百度来的概念解释。我把JS的作用域理解为--标识符的可用性的范围。

    作用域一般分全局作用域、函数作用域、let/const块级作用域。

    先来看看全局作用域。代码演示:

    1 <script>
    2   var name = 'xm';
    3 </script>

    以上代码定义了一个变量name。name的作用域是全局作用域。

    浏览器碰到上面的代码时,会创建一个全局执行环境(也叫全局执行上下文)。

    执行环境(也叫执行上下文)(execution context)是js代码执行的环境。

    全局执行上下文的特点是一个网页只有一个全局执行上下文,关闭网页关闭浏览器时全局执行上下文销毁。不然一直都存在。

    在浏览器环境中,全局执行上下文就是window对象。全局执行上下文里定义的所有的变量和函数都是作为window对象的属性和方法创建的。(let关键字声明的变量除外,但不影响它是全局变量)

    那么,以上代码定义的变量name是全等于window.name的,并在任何地方都可以访问到。


     总结:在全局执行上下文定义的变量、声明的函数。或者通过window.XXX定义的属性,其作用域都是全局作用域。

    函数作用域也叫局部作用域。代码演示:

    1 var name = 'xm';
    2 function foo() {
    3   var age = 18;
    4   console.log(name);
    5 }
    6 foo();

    以上代码在全局执行上下文定义name、foo函数,调用foo()函数。在函数内部定义了age变量,访问name。

    age的作用域就是函数内部。外部访问age会报错。 

    而函数内部访问name是没有问题的。在以前学过的知识里,我们知道这是作用域链的原因。

     后台如何实现作用域链的呢。

    首先,在定义foo()函数时,后台会创建一个预先包含全局变量对象的作用域链。

    然后该作用域链保存在内部属性[[Scope]]中。

     

    变量对象与执行上下文紧紧关联。每个执行上下文都有一个变量对象(variable object)。

    简单理解为存储变量的容器。全局变量对象就是window对象。

    执行上下文被销毁时,对应的变量对象也会随着被销毁。(闭包的情况不同。简而言之,函数执行上下文被销毁了,活动对象(下文有解释)还保留在内存中)

    前言所叙,全局执行上下文始终存在,那么函数foo的执行上下文呢。(叫做函数执行上下文或局部执行上下文)

    函数执行上下文只有在函数执行过程中存在。

    当调用foo函数时。后台会为函数创建一个执行上下文。(叫做函数执行上下文,调用函数时创建。)而且此时,执行流从全局执行上下文,进入函数执行上下文。

    有了执行上下文,就会有一个与之关联的变量对象。在函数执行上下文里,这个变量对象又叫活动对象(active object)(可能因为函数变量对象经常被销毁,故取名活动对象..)

    活动对象最开始只有一个arguments对象(耳熟能详emm...)。

    后续会把命名参数、函数内部定义的变量、函数添加到活动对象中。

    接着复制函数的[[Scope]]属性中的对象构建起函数执行上下文的作用域链。

    然后活动对象被推入函数执行上下文的作用域链的前端。(以上都在函数内部代码执行前完成) 

    如图:

    arguments应该类似于这样:,不是undefined

    再此,可以总结下:

    作用域链是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

    调用foo()函数,当执行到console.log(name)时,会在作用域链中搜索name,搜索顺序先从当前活动对象找,没找到接着去外层变量对象找,一层一层的找。(都找不到,一般会报错)

    如图:

    arguments应该类似于这样:,不是undefined

    变量提升

    执行上下文建立后,变量对象保存当前执行上下文声明的变量,值为undefined。

    定义了多个同名变量。只保存一个。执行到对应代码时,完成赋值。

    访问一个变量时,总是先去当前变量对象上查找。找不到往上一层变量对象找。(如果有的情况下) 全局变量对象为最外层变量对象。在这里也找不到就报错。

    1 console.log(name); // undefined
    2 var name = 'xm';
    3 console.log(name); // xm
    4 var name = 'xh';
    5 console.log(name); // xh

    以上代码,执行到第一行时,引擎去全局变量对象找name,刚好找到了,输出。

    第二行给name重新赋值,第三行输出最新的name。

    第四行有给name重新赋值,第五行输出最新的name。

    函数声明提升

    执行上下文创建时。把function关键字开头的函数声明载入内存中。当前变量对象保存函数名,属性值为指针,指向函数。

    后续执行到对应函数声明时,不会再去执行。

    1 foo(); // xm
    2 function foo() {
    3   console.log('xm')
    4 }

    以上代码,执行到第一行时,引擎会去访问全局变量对象,看有没有foo。有就执行,没有报错。

    当变量声明和函数声明重名时

     当前变量对象保存该名称变量,值为指针,指向函数。执行到对应代码时重新赋值。

    1 console.log(foo); // function foo() {}
    2 function foo() {}
    3 var foo = 'haha';
    4 console.log(foo); // haha

    以上代码,执行到第一行时,引擎会去访问全局变量对象,看有没有foo。有就输出foo,没有报错。

    接着执行到第三行,重新给foo赋值。执行到第四行。输出最新foo。

    在函数内部,变量提升、函数声明提升同理

    此时,变量对象改名叫活动对象,活动对象包含了arguments对象。(全局变量对象没有arguments对象)

    当命名参数与变量名重名时,当前活动对象保存该名称变量,值为命名参数值。代码演示:

    1 function foo(name) {
    2   console.log(name); // xm
    3   var name = 'xh';
    4   console.log(name); // xh
    5 }
    6 foo('xm');

    如图:

    当命名参数、变量名、函数名重名时。当前活动对象保存该名称变量,值为指针,指向函数。

    代码演示:

     1 function foo(name) {
     2   console.log(name); // function name() {}
     3   var name = 'xh';
     4   console.log(name); // xh
     5   function name() {
     6     //
     7   }
     8   name(); // 报错
     9 }
    10 foo('xm');

    如图:

     

    关于let、const

    共同点:都不能声明重名的变量。都不能在声明前访问。定义的变量存在块级作用域。

    不同点:let声明的变量。其值可以改变。const声明的变量,其值不太能改变。当值为引用类型时,可以改变引用类型值的属性。

    关于闭包

    闭包是指有权访问另外一个函数作用域中的变量的函数。

    创建闭包的常用方式:在函数内部创建另一个函数。

    代码演示:

    1 var name = 'xm';
    2 function foo() {
    3   var age = 18
    4   return function () {
    5     console.log(name,age); 
    6   }
    7 }
    8 var foo1 = foo();
    9 foo1(); // xm 18

    以上代码。匿名函数作用域链包含了自己活动对象,foo函数的活动对象,全局变量对象。

    第八行执行完成时,foo函数执行上下文被销毁,重新返回全局执行上下文。

    此时foo的活动对象并未被销毁,匿名函数的作用域链保持了对foo活动对象的引用。foo活动对象扔保留在内存中。

    执行第九行代码,进入匿名函数执行上下文,执行第五行代码,引擎会在匿名函数作用域链去查找name、age。找到输出,找不到报错。

    如图:

    在看一个经典的例子:

    1 var result = [];
    2 for (var i = 0; i < 3; i++) {
    3   result[i] = function () {
    4     return i;
    5   }  
    6 }
    7 // console.log(result[0]());// 3
    8 // console.log(result[1]());// 3
    9 // console.log(result[2]());// 3

    以上代码执行完成,result是一个函数数组,每个函数都会返回一个i。

    似乎每个函数都应该返回自己的索引值,0位置函数返回0,1位置范围1...

    实际上。每个函数返回的都是3。

    通过作用域链可以很好的理解:

    比如:调用result[0]()函数。函数执行上下文创建,推入执行栈,当前函数的活动对象只包含一个变量,即arguments对象。

    外层活动对象是全局变量对象。这里保存着result、i两个变量。此时i值为3。

    查找从当前活动对象开始,没有找到,接着往全局变量对象找,找到i。返回i值。

     要让输出值符合预期。可以使用let声明变量i。(需在for表达式中声明)

    其原理是let声明的变量存在块级作用域。作用域链的查找变为:当前活动对象---块级作用域活动对象---全局变量对象。

    相当于:

    或者:

    1 var result = [];
    2 for (var i = 0; i < 3; i++) {
    3   result[i] = (function(i) {
    4     return function () {
    5       return i;
    6     };
    7   })(i);
    8 }

    作用域链的查找变为:当前活动对象---外层自执行匿名函数的活动对象---全局变量对象。

    其他一些坑:

    1 var a = 1;
    2 var a;
    3 console.log(a); // 1

    全局变量对象保存了a。第一行把1赋值给a。第二行啥也没做。(只声明不赋值。会被忽略。)第三行输出1。

    1 var a = 1;
    2 function a() {
    3   console.log(1);
    4 }
    5 a();// 报错。a is not a function

    function关键字打头的函数声明会在执行上下文创建时载入内存中。变量对象里保存了同名属性指针,指向函数。执行到对应行代码时不会重新声明。

    执行第一行代码,把1赋值给了a。接着到第五行。此时a=1。不是函数。

  • 相关阅读:
    Oracle之SYSDBA的使用
    多表关联查询之内关联,左关联
    oracle 性能大提升
    Oracle_in_not-in_distinct_minsu的用法
    oracle之Sequences
    oracle 基本函数小例子--查询身高段分数段
    oracle 求班级平均分
    转汉字为拼音的字库和代码收集
    filezilla显示隐藏文件
    escapeRegExp捕捉通配符的代码解析
  • 原文地址:https://www.cnblogs.com/caimuguodexiaohongmao/p/11152108.html
Copyright © 2020-2023  润新知