js中的执行环境以及作用域

本文对照js高程进行知识总结整理,详见js高程(第三版)p178-p184。<!--more-->

执行环境和作用域#

执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。

执行环境的类别:

  • 全局执行环境(最外围):随所在宿主环境不同而不同。在浏览器中全局执行环境被认为是window对象,在nodejs中为global。全局执行环境直到应用程序退出时才会被销毁(例如关闭网页和浏览器)。
  • 每个函数都有自己的执行环境:当执行流进入一个函数,函数的环境就推入一个环境栈中,在函数执行之后栈将其环境推出,将控制权返回给之前的执行环境。

变量对象:每个执行环境都有一个与之关联的变量对象(保存在环境中定义的所有变量和函数,我们无法访问,解析器后台处理需要)。某个执行环境中的所有代码执行完毕后该环境被销毁,其中定义的所有变量和函数也随之销毁。

作用域链:代码在执行环境中执行时会创建变量对象的作用域链(保证对执行环境有权访问的所有变量和函数的有序访问)。作用域链的前端始终都是当前执行代码所在环境的变量对象。如果这个环境是函数则将其活动对象作为变量对象。活动对象在最开始时只包含arguments对象一个变量(全局环境不存在arguments)。作用域链的下一个变量对象来自包含(外部)环境,再下一个变量对象来自下一个包含环境,一直延续到全局执行环境(全局执行环境的变量对象始终是作用域链中的最后一个对象)。标识符的搜索从作用域链的前端开始,逐级向后回溯直到找到标识符为止(否则报错)。内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。任何环境都不能通过向下所有作用域链而进入另一个执行环境。作用域链本质上是一个指向变量对象的指针列表,只引用但不实际包含变量对象。

  • 延长作用域链:执行流进入try-catch语句中的catch块或者with语句都会延长作用域链。这两个语句都会在作用域链的前端添加一个变量对象,with语句会将指定的对象添加到作用域链的顶部,catch语句会创建一个新的变量对象(包含被抛出的错误对象的声明)。
  • 没有块级作用域:使用var声明的变量会自动被添加到最接近的环境中(如if、for语句中的var声明的变量,不像其他类C语言由花括号封闭的代码块都有自己的作用域)。如果不使用var声明而是直接初始化就会自动被添加到全局环境中。

函数和变量提升的原因:通常的解释是说将声明的代码移动到了顶部(便于理解),更准确的解释:在生成执行环境时有两个阶段:第一个阶段是创建变量对象,JS解释器会找出需要提升的变量和函数,并且给它们提前在内存中开辟好空间,将整个函数存入内存中,变量只声明并且赋值undefined;第二个阶段就是代码执行,可以直接使用。

闭包#

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数,内部函数的作用域链中包含外部函数的作用域(内部函数可以访问外部函数中定义的变量)。

全局环境的变量对象始终存在,类似函数这样的局部环境的变量对象只在函数执行的过程中存在,创建函数时会创建一个包含全局变量对象的作用域链(保存在内部的 [[Scope]]属性中),调用函数会为函数创建一个执行环境,然后复制函数的 [[Scope]]属性中的对象构建起执行环境的作用域链,然后创建函数的活动对象并推入执行作用域链的前端。通常函数执行完毕后局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。

闭包中的特殊之处在于:闭包中的内部函数返回之后,其作用域链被初始化为包含包裹它的外部函数的活动对象和全局变量对象,在包裹它的外部函数执行完毕后(返回内部函数后)这个外部函数执行环境的作用域链会被销毁,但其活动对象仍然留在内存(内部函数的作用域链仍在引用这个活动对象),直到内部函数被销毁后外部函数的活动对象才会被销毁。

闭包中的副作用:闭包只能取得包含函数中任何变量的最后一个值(1.闭包保存的是整个变量对象,即共享词法作用域;2.var变量提升)。可以利用IIFE(立即执行函数)模拟一个块级作用域(函数有自己的作用域),实际上就是声明一个匿名函数,同时立即执行它自己。详见js预解析和作用域

闭包的好处:

  • 允许将函数同与其所操作的某些数据(环境)关联起来,类似于面向对象编程中对象允许将某些数据(对象属性)与方法相关联。

  • 可以用来模拟私有方法,不仅可以限制对代码的访问,还提供管理全局命名空间的能力

    var makeCounter = function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }
    };
    
    var Counter1 = makeCounter();
    var Counter2 = makeCounter();
    console.log(Counter1.value()); /* logs 0 */
    Counter1.increment();
    Counter1.increment();
    console.log(Counter1.value()); /* logs 2 */
    console.log(Counter2.value()); /* logs 0 */
    

预解析#

所谓预解析,就是在当前作用域之下,在js代码运行之前,会把带有var和function关键字的事先声明,但是不会赋值。

// 一道面试题
alert(a) // Function a
a(); // alert(10)
var a=3;
function a(){
    alert(10)
}   
alert(a) // 3
a=6;
a(); // TypeError: a is not a function

所考到的知识点有两个:

  • 事先进行变量声明(即预解析,将变量提升到顶部)
  • 函数声明优先于变量声明

所以运行结果就是

  1. 刚开始,a就是 function a(){alert(10)},就会看到这个函数
  2. a(),执行函数,alert(10)
  3. 执行了 var a=3,所以 alert(a)显示3
  4. 然后 a就不是一个函数,再往下执行 a()就会报错
//类似相关
alert(a) // undefined
a(); // TypeError: a is not a function
var a=3;
var a=function(){
    alert(10)
}   
alert(a) // Function a (需要注释掉第二行)
a=6;
a(); // TypeError: a is not a function

这里有一个函数表达式,将一个匿名函数(没有名字的函数)赋值给一个变量,此时变量a的值就是指向函数对象的指针,从本质上来说和使用函数声明的方式定义一个有名字的函数是一样的,与第一个的区别就在这里,将函数的声明转化为了这种var型变量的声明,由于预解析只是进行事先声明,但并不进行赋值操作,所以一开始第一个 alert(a)显示的是 underfind,然后执行 a()的时候会报错的原因和刚开始的一样,转化为var型之后就没有函数的声明优先于变量的声明,两个都是变量(即匿名函数声明等同于变量声明),这个时候 a就是一个函数。

暂时性死区#

什么是暂时性死区?只要区块(块级作用域)中存在 let、const 命令,这个区块对这些命令声明的变量,从一开始就形成封闭作用域,凡在声明之前使用这些变量就会报错

为什么要有暂时性死区?暂时性死区和 let、const 语句不出现变量提升,主要是为了减少运行时错误,防止变量声明之前就使用这个变量

var tmp = 123
if (true) {
	tmp = 'abc' // referenceError
	let tmp
}

typeof x // referenceError
let x
typeof undeclared_variable // undefined

面试题#

//收录一些面试题
var a=0;
function aa(){
    alert(a)
    a=3
}
//结果是什么都没发生,因为要执行aa函数才会执行alert(0)

//------------分割线1------------------

var a=0;
function aa(){
    alert(a)
    var a=3
}
aa();
//underfind  在aa函数里面,有var a=3,那么在aa作用域里面,就是把a这个变量声明提前,但是不会赋值,所以是underfind

//------------分割线2------------------

var a=0;
function aa(a){
    alert(a)
    var a=3
}
aa(5)
alert(a)
//5,0   在函数体内,参数a的优先级高于变量a

//------------分割线3------------------

var a=0;
function aa(a){
    alert(a)
    a=3
}
aa(5)
alert(a)
//5,0   在函数体内,执行alert(a)和a=3,修改的的并不是全局变量a,而是参数a

//------------分割线4------------------

var a=0;
function aa(a){
    alert(a)
    var a=3
    alert(a)
}
aa(5)
//5,3

//------------分割线5------------------

var a=0;
function aa(a){
    alert(a)
    a=3
    alert(a)
}
aa()
alert(a)
//underfind  3  0 
/*首先,参数优先级高于全局变量,由于没传参数,所以是underfind
a=3,实际上修改的时形参a的值,并不是全局变量a,往下alert(a)也是形参a*/

循环内变量过度共享#

for (var i = 0; i < 5; i++) {
 setTimeout(function() {
  console.log(i);
 }, 1000);
}
console.log(i);
//这个大家就要小心一点了,答案是5    55555
//在setTimeout执行之前,for循环早就执行完了,i的值早已经是5了,所以一开始是执行,最后面的console.log(i);
//在for循环的时候一下子自定义5个setTimeout,大概一秒后,就是输出55555

首先要得到这个结果就必须对JavaScript线程有一定了解

这道题的考察点:JavaScript的单线程以及setTimeout的异步特性

JavaScript引擎是单线程运行的,浏览器运行期间只有一个线程在运行js程序。浏览器的内核是多线程的,他们在内核控制下相互配合,以保持同步,一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。

  • JavaScript引擎是基于事件驱动单线程执行的,js引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个js线程在运行js程序。
  • GUI渲染线程负责浏览器界面的渲染,当界面需要重绘的时候或者由于某种操作引发回流时,该线程就会执行,需要注意GUI渲染线程与js引擎是互斥的,当js引擎执行的时候GUI线程会被挂起,GUI更新会被保存在一个队列中,等到js引擎空闲时立即被执行
  • 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理的队列末尾,等待js引擎的处理。这些事件可来自js引擎当前执行代码块,如setTimeout,也可来自浏览器内核的其他线程如鼠标点击、ajax异步请求等,但由于js的单线程关系所有这些事件都得排队等待js引擎处理。
  • 当线程中没有任何同步代码的前提下才会执行异步代码。

所以在上题之中setTimeout是异步的代码,即使是setTimeout中设置的等待时间为0也不会立即执行,而for循环代码是同步的,所以要等待for循环执行完以后才会执行setTimeout。

关于为什么都是5:因为setTimeout中的i是对外层i的引用,当setTimeout的代码被解释的时候,运行时只是记录了i的引用,而不是i的值。这样当setTimeout被触发的时候,setTimeout被同时取值,由于他们都是指向了外层的同一个i,而此时i的值是5,所以打印的都是5.

同样问题分析:#

var s = document.getElementsByTagName('input'); //获取整个网页的标签
    window.onload = function(){
        for(var i = 0;i < s.length;i++){
            s[i].onclick = show(i);
        }
    };
    function show(num){
        alert("你好,请"+num+"号嘉宾领奖")
    }

这个例子实际运行点击无论哪个按钮,都会alert相同的值(s.length)

JavaScript没有块级作用域,用循环赋值给每一个 s[i] 的方法的参数都是一个i的引用,它们都指向同一个也是唯一一个i,所以最终无论点哪个按钮,alert 的都是同一个值,在这个循环中,i的值就等于 s.length。

解决方案:

window.onload=function(){
 for(var i=0;i< s.length;i++){
  s[i].onclick=(function(num){
  	return function(){show(num)}
  })(i)//采用的是直接调用函数表达式(免去了赋值再调用)
 }     //需要再函数体外围加上括号,然后再以括号调用
};
function show(num){
 alert("你好,请"+num+"号嘉宾领奖")
}

采用的写法是利用IIFE(立即执行函数)模拟了一个块级作用域(函数有自己的作用域),实际上就是声明一个匿名函数,同时立即执行他自己。

匿名函数可以用来做闭包

finciton F(){
    var name = "a";
    return function(){
        alert(name);
    };
}
var closure = F();
closure();

F() 的返回值是一个内部函数,这个函数可以访问外部函数的作用域,也就是可以访问到外部函数的变量。

注意:因为把返回的函数赋给了一个变量,虽然函数在执行完一瞬间会销毁其执行环境, 但是如果有闭包的话,闭包会保存外部函数的活动对象(变量),所以如果不把对闭包的引用消除掉,闭包会一直存在内存中,垃圾收集器不会销毁闭包占用的内存。

这从某种程度上说也是 var型变量的bug,即没有块级作用域和循环内变量过度共享,解决的方法在ES6中新提出的类型,即 letconst,let声明的变量有块级作用域(所以循环内的let变量到循环外不能被使用)并且let声明的全局变量不是全局变量的属性(它们存在于一个不可见的块的作用域中,不能通过window.变量名的方式访问这些变量),let 没有变量提升。

这个时候可以采用匿名函数 闭包的概念解决这个问题

for (var i = 0; i < 5; i++) {
 (function(j) { // j = i
  setTimeout(function() {
   console.log(j);
  }, 1000);
 })(i);
}
console.log(i); 
//这里的解析和上面基本一样,只是用闭包来记录每一次循环的i,
//所以答案是5     01234



var output = function (i) {
 setTimeout(function() {
  console.log(i);
 }, 1000);
};
 
for (var i = 0; i < 5; i++) {
 output(i); // 这里传过去的 i 值被复制了
}
console.log(i);

//这里的解析和上面基本一样,把i当参数传进output,记录每一次循环的i,
//所以答案是5     01234

闭包、this常见面试题:

参考

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);
//问:三行a,b,c的输出分别是什么?
var a = 10;
var obj = {
    a:20,
    say:()=>{
        console.log(this.a)
    }
}
var objTemp = {
  	a:20,
    say:function () {
      console.log(this.a)
    }
}
obj.say();
var obj2 = {a:30}
obj.say.apply(obj2);
// obj 对象不能产生作用域,箭头函数中的 this 始终指向其父作用域中的 this
objTemp.say()
objTemp.say.apply(obj2)
function fun () {
    return () => {
        return () => {
            return () => {
            					console.log(this.name)
                    }
                }
        }
}
var f = fun.call({name: 'foo'})
var t1 = f.call({name: 'bar'})()()
var t2 = f().call({name: 'baz'})()
var t3 = f()().call({name: 'qux'})
var name = 'global';
function Person(name) {
  this.name  = name;
  this.sayName = () => {
  	console.log(this.name)
	}
}
const personA = new Person('aaa');
const personB = new Person('bbb');
personA.sayName();
personB.sayName();

作者:EGBDFACE

出处:https://www.cnblogs.com/EGBDFACE/p/16267526.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   EGBDFACE  阅读(58)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示