Bootstrap

JavaScript原理篇——深入理解作用域、作用域链、闭包、this指向

执行上下文描述了代码执行时的环境,包括变量对象、作用域链和 this 值;而作用域则决定了变量和函数的可访问性范围,分为全局作用域和局部作用域。

变量对象用于存储变量和函数声明:是与执行上下文相关联的数据结构,用于存储在上下文中定义的变量、函数声明和形参等信息。

作用域链用于解析标识符的查找路径:是 JavaScript 中用于解析标识符(变量名)的机制,它由多个执行上下文的变量对象组成的链表结构。当代码在某个执行上下文中查找变量时,会首先查找当前上下文的变量对象,如果找不到,则会沿着作用域链向上一级上下文查找,直到找到为止。作用域链的形成是由函数的嵌套关系所决定的,内部函数可以访问外部函数的变量,但外部函数无法访问内部函数的变量。

this 值取决于函数的调用方式:this 是 JavaScript 中的一个关键字,它在函数被调用时绑定到函数的执行环境。this 的值取决于函数的调用方式。在全局作用域中,this 指向全局对象(在浏览器中通常是 window 对象)。在函数内部,this 的值取决于函数被调用时的上下文。可以通过函数的调用方式(作为方法、通过 call、apply、bind 方法等)来改变 this 的值。

测试你对作用域的掌握程度

请你输出下面代码的执行结果

  

如果你毫无头绪的话,请尝试阅读本文。在本文最后的挑战作用域笔试题中给出答案。

 执行上下文与作用域

行上下文(Execution Context)和作用域(Scope)是 JavaScript 中重要的概念。理解这两个概念对于编写和调试 JavaScript 代码至关重要,因为它们直接影响了代码中变量和函数的可见性和生命周期。

执行上下文(Execution Context)

执行上下文是 JavaScript 中一个抽象的概念,它定义了代码执行时的环境。每当 JavaScript 代码执行时,都会创建一个执行上下文,并将其推入执行上下文栈(Execution Context Stack)中。执行上下文在js中很重要。变量或函数的上下文决定了它们能访问哪些数据,以及行为。每个上下文都关联一个变量对象,在这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。

执行上下文包括三个重要的组成部分:

  • 变量对象(Variable Object):用于存储变量、函数声明和函数参数。在全局上下文中,它被称为全局对象(Global Object),在函数上下文中,它被称为活动对象(Activation Object)。
  • 作用域链(Scope Chain):用于查找变量的链条,它由当前执行上下文的变量对象和所有包含(父级)执行上下文的变量对象组成。
  • this 值:指向函数执行的当前对象,在全局上下文中通常指向全局对象,在函数上下文中取决于函数的调用方式。

this是什么

this 是一个关键字,其工作原理是根据函数的调用方式动态确定其指向的对象。具体来说:

  • 当函数作为普通函数调用时,this 指向全局对象(在浏览器中通常是 window 对象)。
  • 当函数作为对象的方法调用时,this 指向调用该方法的对象。
  • 当函数作为构造函数调用时(使用 new 关键字),this 指向新创建的实例对象。
  • 当函数作为事件处理函数绑定到 DOM 元素上时,this 指向触发事件的 DOM 元素。

全局作用域

// 全局作用域
console.log(this); // 指向全局对象(window)

 对象方法中的this

const obj = {
  name: "Alice",
  greet() {
    console.log(this.name);
  },
};
obj.greet();

构造函数方法的this

function Person(name) {
  this.name = name;
}
const person1 = new Person("Bob");
console.log(person1.name);

this的值是在执行的时候才能确认,定义的时候不能确认

上下文栈

当函数被调用时,函数的上下文会被推到一个上下文栈上。在函数执行完后,上下文栈会弹出该函数上下文。将控制权返回给之前的上下文。

闭包怎么产生的

上下文所在代码都执行完毕后才会销毁。这也是闭包存在的作用,如果有代码还没执行完某些变量无法被销毁。闭包是指函数和函数内部能访问到的其外部作用域的变量的组合。在 JavaScript 中,函数可以访问其定义时所处的词法作用域,即使函数是在其定义的作用域之外执行的。

通常来说,栈的内存小,基本数据类型用栈存储,引用数据类型用堆存储。引用类型在栈中国存了查找引用对象在堆内存的地址。但是闭包中的变量存在哪里呢?

如果变量存在栈中,那函数调用完栈顶空间销毁,闭包变量不就没了吗?

闭包变量是存在堆内存中的。

function outerFunction() {
  let outerVar = 'I am from outer function';
  function innerFunction() {
    console.log(outerVar);
  }
  return innerFunction;
}
const innerFunc = outerFunction();
innerFunc(); // 输出:I am from outer function
  • 上面的代码输出:I am from outer function。因为 innerFunction 是在 outerFunction 内部定义的,可以访问 outerFunction 的变量 outerVar

  • 闭包的优点包括可以实现数据封装、延长变量的生命周期、实现模块化等;缺点包括可能导致内存泄漏(如果不注意释放闭包)、影响性能(因为变量未被及时释放)等。

作用域(Scope)

作用域是指变量和函数的可访问性范围,它决定了在代码中的哪些位置可以访问到某个变量或函数。变量的作用域是在定义时就决定了。JavaScript 中有三种主要类型的作用域:

  • 全局作用域(Global Scope):在整个代码中都可访问的作用域,任何在全局作用域中声明的变量或函数都可以被任何地方的代码访问。
  • 局部作用域(Local Scope):在特定代码块(通常是函数)中可访问的作用域,局部作用域可以嵌套,内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。
  • 块级作用域(Block Scope):块级作用域指的是由一对花括号 {} 包裹起来的代码块内部所创建的作用域。在 JavaScript 中,使用 letconst 关键字声明的变量具有块级作用域,即只在声明它们的代码块内部可见。块级作用域可以帮助我们避免变量污染和提供更好的封装性。

全局作用域

根据js实现的宿主环境,在浏览器中,全局上下文就是window对象。所有通过var定义的全局变量和函数都会成为window对象的属性和方法。 全局上下文只有在应用程序退出前才会被销毁。比如关闭网页或退出浏览器

局部/函数作用域

每个函数都有自己的上下文。注意是function定义的函数才有自己的上下文。不是用了大括号的都有上下文,像for循环和if是不会创建自己的上下文的。

作用域链

作用域链的创建是在函数定义时确定的,它与函数的定义位置有关。当函数被调用时,会创建一个新的执行环境,其中包含一个新的变量对象,并将其添加到作用域链的前端。这样,函数内部就可以访问其所在作用域以及其外部作用域中的变量和函数,形成了一个作用域链。

作用域链的作用

对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]]属性查找上级变量。作用域链是一种用于查找变量和函数的机制,它是由当前执行环境和其所有父级执行环境的变量对象组成的链式结构。当在一个执行环境中访问变量或函数时,会首先在当前执行环境的变量对象中查找,如果找不到,则会沿着作用域链向上查找,直到找到对应的变量或函数,或者达到最外层的全局对象(如window)。

简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期 

作用域查找示例

function outer() {
  var outerVar = "Outer variable";

  function inner() {
    var innerVar = "Inner variable";
    console.log(innerVar); // 内部作用域的变量
    console.log(outerVar); // 外部作用域的变量
    console.log(globalVar); // 全局作用域的变量
  }

  inner();
}

var globalVar = "Global variable";
outer();

 示例中,函数inner()内部可以访问到其外部函数outer()中定义的变量outerVar,这是因为inner()的作用域链中包含了外部函数的变量对象。同样,inner()也可以访问全局作用域中的变量globalVar,因为全局作用域也在作用域链中。

通过作用域链的机制,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部的变量,这就实现了变量的封装和保护。

不要忽略作用域内部的变量提升

作用域链查找只能向上查找,不能向下。但在实际变量被读取时要考虑当前作用域内是否有变量被提升,根据不同类型定义的变量,var会被提升,未赋值时读取是undefined,const 和let在所在块级作用域内部被提升,未赋值时读取报未初始化错误。在介绍var、let和const的时候给出详细的例子。

函数声明、函数声明提升

  1. 函数声明提升(Function Declaration Hoisting):JavaScript 中的函数声明会在当前作用域的顶部被提升,意味着可以在声明之前调用函数。这意味着你可以在函数声明之前调用函数,而不会出现引用错误。

  2. 全局作用域中的函数声明会成为全局变量:在全局作用域中声明的函数会成为全局对象的属性,可以通过全局对象访问到。例如,在浏览器环境中,全局对象是 window,因此全局函数可以通过 window 对象访问。

  3. 函数声明语法:函数声明使用 function 关键字来声明,后面跟着函数名和一对圆括号,圆括号中包含参数列表。函数体包含在一对花括号中。

  4. 可以重复声明:在同一作用域中,可以多次声明同名的函数,后面的声明会覆盖前面的声明。

  5. 函数声明不是语句(Statement):在 JavaScript 中,函数声明不是语句,因此不能出现在条件语句(如 ifwhile 等)或循环语句(如 fordo-while 等)的块中。在这些位置,应该使用函数表达式。

通过function声明的函数会被提升

在执行代码时,js引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此,即使函数定义在调用之后,也不会出错。

console.log(sum(10, 20));
function sum(num1, num2) {
  return num1 + num2;
}

 

函数表达式不能提升

但是通过下面这种写成函数表达式的不能进行函数提升,只是函数名的提升。在为定义前使用会报错

console.log(sum(10, 20));
var sum = function (num1, num2) {
  return num1 + num2;
};

 这里执行会报错,因为sum被当成变量被提升了,sum在内存中是undefined。找不到函数体,因此会报错,sum is not a function

变量声明 、变量提升

当涉及到 JavaScript 中的变量声明和变量提升时,letconst 和 var 有着不同的行为:

  1. var 声明

    • var 声明的变量存在变量提升(hoisting):在执行上下文中,变量声明会被提升到函数或全局作用域的顶部,但赋值不会被提升。
    • var 声明的变量在其声明的整个函数范围内是可见的。
    • 可以重复声明相同的变量名,后面的声明会覆盖前面的声明。
  2. let 声明

    • let 声明的变量存在块级作用域,let会提升,但不会被提升到块级作用域之外,且提升也不能读取,存在暂时性死区,未赋值前读取会报错。
    • let 声明的变量在其声明的块级作用域内可见,包括 {} 内部的任何代码块。
    • 不允许在同一个作用域内重复声明同名的变量。
  3. const 声明

    • const 声明的是一个常量,常量的值在声明后不能被修改。
    • const 声明的行为与 let 类似,也存在块级作用域,跟let一样,会提升声明,存在暂时性死区,未赋值前访问会报错。
    • 常量必须在声明时初始化,且不能重新赋值。
    • 与 let 一样,不允许在同一个作用域内重复声明同名的变量。

未声明的变量

如果直接读取未声明的变量是会报错的。但是对未声明变量进行赋值操作会自动生成一个全局变量

 能力自测:下面代码输出结果

function add(num1, num2) {
  Asum = num1 + num2;
  return Asum;
}
let res = add(10, 20);
console.log(Asum);

正常来说,Asum是在函数内部使用的,在外部不能访问。但是Asum没有在函数内部定义啊。JS在处理Asum赋值的时候自动创建了一个全局变量Asum,这样赋值后再访问Asum就可以了。看下debugger过程,这里起名Asum也是快速找到全局作用域中的变量。

因此,答案是30

var声明变量

  • var 声明的变量存在变量提升(hoisting):在执行上下文中,变量声明会被提升到函数或全局作用域的顶部,但赋值不会被提升。
  • var 声明的变量在其声明的整个函数范围内是可见的。
  • 可以重复声明相同的变量名,后面的声明会覆盖前面的声明。

使用var声明的变量会被自动添加到最近的上下文环境中;js引擎在编译的时候会将变量的声明被提升到所在函数或全局作用域的顶部,位于作用域中所有代码执行之前。注意:var的变量提升只在最近的上下文进行提升,不会提升到作用域的外部!在变量作用域内,如果使用了var定义变量,由于var声明提升,未赋值前访问不会报错,而是undefined。在同一作用内,var也可以重复声明,不会报错 ,重复的声明会被忽略。

函数声明和var声明重复了如何处理?

当函数的名称和var变量名称重复了。那么不管声明的先后顺序,函数的声明永远高于变量的声明。但是在赋值语句的时候要跟顺序有关了。后赋值的优先先赋值的。

你真的学会了吗,请按下面顺序,依次写下你认为的结果,题目后面给出解析。

 能力自测1:下面代码的输出结果

提示:考察var变量提升

function foo() {
  console.log(a);
  var a = 5;
  console.log(a);
}

foo();

能力自测2:下面代码的输出结果

var myVar = "global";
function func() {
  console.log(myVar); // 输出什么?
  var myVar = "local";
  console.log(myVar); // 输出什么?
}
func();

能力自测3:下面代码的输出结果

提示:考察变量和函数名重名以及作用域链的概念

var a = 1;
function b() {
  a = 10;
  return;
  function a() {}
}
b();
console.log(a);

自测题答案

能力自测1

function foo() {
  console.log(a);
  var a = 5;
  console.log(a);
}

foo();

答案:

undefined

5

思路:这个题很简单,考察var的变量提升,未赋值就能访问到undefined值。赋值后访问的是5

 能力自测2

var myVar = "global";
function func() {
  console.log(myVar); // 输出什么?
  var myVar = "local";
  console.log(myVar); // 输出什么?
}
func();

答案:

undefined

local

首先,myVar会在函数作用域func内部找有没有var声明的变量,找到了,输出undefined。接着,myVar被赋值,输出local。当前作用域有值就不会沿着作用域链向上找

能力自测3

var a = 1;
function b() {
  a = 10;
  return;
  function a() {}
}
b();
console.log(a);

答案:输出1

函数内部有同名函数function a,因此,function a会被在function b函数作用域内进行提升。在访问第3行,debugger语句的时候,a是function a{}。第4行,a被复制为10。由于js是弱类型语言,因此不会报错,a=10,但是这个a是function b函数作用域里声明的a。是局部作用域a。全局作用域a没有被改变。如果function b里没有定义function a,那么a=10在自身函数作用域里找不到a才会找外部的。因此这个题输出的a=1

let声明变量、形成块级作用域

块级作用域怎么产生的?

通过let声明的变量,会产生块级作用域。let声明的块级作用域范围是由最近的一对花括号界定的。因此,if(){}块,while(){}块、function(){}块、{}块都可以是let声明的块级作用域范围。let声明的变量只能在块级作用域内部访问。

块级作用域和全局作用域、函数作用域并称JS的三个作用域体系。

let声明的变量有以下特性:

 暂时性死区:let的另一特性是和var比,let定义的块级作用域变量,在js预处理时也会被进行变量声明的提升,因此,在块级作用域内部,可以看到变量在未赋值时也能看到内存有一个undefined值的变量。但是你不能读取它,这是let和var的区别,var的变量提升是可以读取的,let不行,let有暂时性死区,在赋值前不能读操作。

 重复声明会报错:let和var的另一个不同之处在于,通过let定义的变量,在同一作用域内不能被声明两次,重复声明的var会被忽略,但是重复通过let声明的变量会报错。

能力自测1:下面代码的输出结果

function fun() {
  if (true) {
    var a;
  }
  console.log(a);
}
function fun2() {
  if (true) {
    let a;
  }
  console.log(a);
}
fun();
fun2();

 分别用if语句包括了var变量和let变量,在if语句的外部输出变量。

通过var声明的变量a在fun函数作用域内,console.log(a)在函数fun作用域内,因此可以访问a。输出1。

但是fun2函数,不仅有函数作用域,通过let声明的变量还会创建一个block块级作用域。可以通过下面debugger过程左侧变量看出来。

前面说的let会创建一个块级作用域,let所在的花括号内部是let的作用域,因此,fun2函数在外部打印a会报错。

能力自测2:下面代码的输出结果

function fun() {
  if (true) {
    console.log(a);
    var a = 1;
  }
}
function fun2() {
  if (true) {
    console.log(a);
    let a = 1;
  }
}
fun();
fun2();

题中if语句中各自通过var和let声明变量;并且在声明前进行console.log读取。

var声明的变量在所处的fun函数作用域内进行提升,在赋值之前是undefined,因此会输出undefined。

let声明的变量会在if花括号范围的Block块级作用域内进行提升,是undefined,但是无法进行读操作,因为let有暂时性死区不让读。因此会报错。

通过debugger看下编译器执行过程中块级作用域let声明的变量

const声明常量、形成块级作用域

const声明的变量必须在声明的同时进行初始化。一经声明,不能被修改。

与let相同,const声明的变量也有块级作用域;const声明的变量也会提升,存在暂时性siq

 通过debugger看下const形成的Block块级作用域

 const声明的如果是引用类型,引用对象里面的内容可以修改,但是引用地址不能变。像下面的代码如果给b添加一个属性是可以的,但是如果更改b的指向,会报错。

function fun() {
  if (true) {
    const b = {};
    b.a = 1; //可以更改对象里的属性
    // b = { 2: 3 };//不能切换b的引用,会报错
    console.log(b);
  }
}
fun();

 挑战作用域的笔试题

 在JS红宝书这样描述了JS的变量特性:JS变量是松散类型的,变量不过是特定时间一个特定值的名称而已。由于没有规则定义变量必须包含什么数据类型,变量的值和数据类型在脚本生命周期可以改变。这就导致了变量可能被多次修改,数据类型可能变化,也有可能被同名函数给覆盖。变量名、函数名、作用域之间演变出了各种复杂的笔试题。只有深入理解作用域的概念、变量提升、函数提升的概念,你才能顺利做出下面的题目。准备好了吗?

考察var的理解

console.log(a);
var a = 100;

fn("zhangsan");
function fn(name) {
  age = 20;
  console.log(name, age);
  var age;
}

console.log(b);
b = 100;

思路:var声明的变量有变量提升特性,未赋值也能访问。由于声明可以被提升,因此声明可以在赋值之后。b没有被声明,赋值语句时js会创建一个var变量b,未赋值前输出undefined。

因此答案输出依次为

undefined

zhangsan,20

undefined

考察var、作用域的理解

let res = new Array();
for (var i = 0; i < 10; i++) {
  res.push(function () {
    return console.log(i);
  });
}
res[0]();
res[1]();
res[2]();

 思路:分析通过var定义的变量,在for循环内,既没有块级作用域,也没有函数作用域。因此,i是一个全局变量。function匿名函数,创建的时候res存储了十个匿名函数,此时十个函数被推执行栈。函数内部引用了函数外部的i,形成了闭包。在函数出栈的时候,此时i变成了10,入栈的函数存的不是值而是变量i。这就导致输出的结果是10个10

如果将var换成let,那么i就形成块级作用域,可以正常按0-9的顺序打印。

当使用 let 声明变量 i 时,i 变量会被绑定在每次迭代中的块级作用域中,而不是像 var 那样绑定在全局作用域中。因此,每次循环迭代时,都会创建一个新的 i 变量,并且每个函数捕获的是对应迭代中的 i 的值。

在这种情况下,每个函数都捕获了不同的 i 变量,而不是共享同一个全局变量。因此,当调用 res[0]()res[1]()res[2]() 时,它们分别打印出了对应迭代中的 i 的值,而不是都打印出 10。这是因为每个函数捕获的 i 都是在其创建时的值,而不受后续循环的影响。

因此,使用 let 声明变量 i 可以避免循环中常见的闭包陷阱

考察var作用域理解2

var name = "World!";
(function () {
  if (typeof name === "undefined") {
    var name = "Jack";
    console.log("Goodbye " + name);
  } else {
    console.log("Hello " + name);
  }
})();

 思路:在function内部,var定义的name进行声明提升,因此,在if执行前name是undefined,注意var没有块级作用域,所以if{}不会限制var的访问。if为true,输出Goodbye Jack

考察作用域

var a = 10
function foo(){
    console.log(a)
}
 
function bar() {
    var a = 20
    foo()
}
 
bar()

 foo函数中的a究竟是哪个呢

要记住,在 JavaScript 中,变量的查找是在定义时的词法作用域中进行的。因此,当 foo() 函数被调用时,它会向上查找作用域链,找到了外部作用域中的全局变量 a,并输出了全局变量 a 的值,即 10。所以最终输出的结果是 10

考察let块级作用域

var a = 1;
function fun() {
  if (false) {
    var a = 10;
    let b = 20;
  }
  console.log(a);
  console.log(b);
}
fun();

思路:预处理的时候a被提升到fun函数作用域的顶部,let被提升值if块级作用域的顶部。但是let提升也不能被访问。由于if语句是false,因此,尽管a的声明被提升,a依旧没被赋值。因此在console.log(a)的时候能在局部作用域找到a的声明,输出undefined。而b已经脱离的块级作用域,b不能访问,报错。

答案:

undefined

报错:ReferenceError: b is not defined思

let暂时性死区

function foo() {
  var a = 1;
  if (true) {
    console.log(a);
    let a = 2;
  }
}

foo();

在if语句里用let声明了a。a被提升在if块的顶部。执行console.log之前已经有局部变量a了。只是let的暂时性死区导致无法读操作,输出ReferenceError: Cannot access 'a' before initialization

考察this1

var a = 2;
const foo = {
  bar: function () {
    console.log(this.a);
  },
  bar1: function () {
    return function () {
      console.log(this.a);
    };
  },
  a: 1,
};
foo.bar();
var bar1 = foo.bar1();
bar1();

 考察this的指向,在对象方法中,this执行调用的对象。普通方法中,this指向执行时所在词法作用域对象。在本题中,foo.bar()执行的是foo对象的bar方法,this执行foo对象,所以this.a=1;在bar中,接收一个return返回的函数,此时bar1是一个全局作用域中的函数,通过bar1()进行函数执行,函数在上下文空间中this指向全局对象,因此输出2。

 考察this2

var name = "Global";

var person = {
  name: "Alice",
  sayHello: function () {
    console.log("Hello, " + this.name);

    function innerFunction() {
      console.log("Hi, " + this.name);
    }

    innerFunction();
  },
};

person.sayHello();
  1. 当调用 person.sayHello() 方法时,首先会输出 "Hello, Alice"。这是因为在 sayHello 方法内部,this.name 指向 person 对象的 name 属性,即 "Alice"

  2. 在 sayHello 方法内部的 innerFunction 函数中,由于 innerFunction 是在全局作用域中定义的普通函数,而不是作为对象的方法调用,因此其中的 this 指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。在vscode里执行是node.js环境,全局对象global中并没有 name 属性,因此 this.name 的值为 undefined。因此,输出为 "Hi, undefined"

因此,这段在浏览器代码的输出是 "Hello, Alice" 和 "Hi, Global"

在node.js环境(vscode)输出是“Hello, Alice”和"Hi,undefined"

考察this3

var counter = {
  count: 0,
  increase: function () {
    this.count++;
    console.log(this.count);
  },
};

var increaseFunc = counter.increase;
increaseFunc();
counter.increase();

看到this就想,this是执行时确定的

  1. 在调用 increaseFunc() 时,由于 increaseFunc 是从 counter.increase 中提取出来的函数,并且在全局作用域中调用,因此函数内部的 this 指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。由于全局对象中并没有 count 属性,因此 this.count++ 操作会导致 count 属性的值变为 NaN

  2. 在调用 counter.increase() 时,increase 方法是作为 counter 对象的方法调用的,因此其中的 this 指向 counter 对象。this.count++ 操作会使 count 属性的值增加 1,然后输出为 1

因此,这段代码的执行结果是 NaN和 1

考察异步方法中的this

function outerFunction() {
  var name = "Global";

  function innerSayHello() {
    console.log("Hi," + this.name);
    let _this = this;
    setTimeout(function () {
      console.log("Hello, " + this.name);
      console.log("Hello," + _this.name);
    }, 1000);
  }

  innerSayHello.call({ name: "Inner" });
}

outerFunction();
  1. 在调用 innerSayHello.call({ name: "Inner" }) 时,innerSayHello 函数被调用,并且使用 call 方法将函数内部的 this 指向了 { name: "Inner" } 对象。因此第一个 console.log 输出为 "Hi, Inner",因为此时 this.name 指向了 { name: "Inner" } 中的 name 属性。

  2. 第二个 console.log 输出为 "Hello, Global",setTimeout是异步函数,由于js事件循环机制先执行同步在执行异步,因此执行异步的时候所在环境是全局对象,因此在回调函数中的 this 指向全局对象。

  3. 在 setTimeout 中的回调函数中,使用了 _this 变量来保存外部函数中的 this 对象,即 { name: "Inner" }。因此第三个 console.log 输出为 "Hello, Inner",因为使用了保存的 _this 变量来访问外部函数中的 name 属性。

因此,这段代码的输出结果是 "Hi, Inner""Hello, Global" 和 "Hello, Inner"

考察闭包、函数作用域

function createCounter() {
  var count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

var counter1 = createCounter();
counter1();
counter1();

var counter2 = createCounter();
counter2();
counter2();

解析:counter1是一个函数,函数内部count引用了定义时词法作用域里的count。因此count是一个闭包;然后执行counter1方法,输出1,再执行,count仍然存储之前定义的值,输出2.而count2通过函数createCounter返回了一个新的匿名函数,拥有自己的独立的地址空间,虽然引用了count,但这个count和counter1的count是独立的。因此,继续输出1,2

 答案:

1

2

1

2

立即执行函数IIFE

var b = 10;
(function b() {
  b = 20;
  console.log(b);
})();

 思路:function b是立即执行函数,在函数内部,b是函数名,函数的内部b=20,由于没有声明b,此时b是对外部全局变量的修改,全局变量b=20。之后打印b,此时b在函数内部表示函数名,因此打印函数b。

考察var+this+作用域链

var b = "boy";
console.log(b);
function fighting() {
  console.log(a);
  console.log(c);
  if (a === "apple") {
    a = "Alice";
  } else {
    a = "Ada";
  }
  console.log(a);
  var a = "Andy";
  middle();
  function middle() {
    console.log(c++);
    var c = 100;
    console.log(++c);
    small();
    function small() {
      console.log(a);
    }
  }
  var c = (a = 88);
  function bottom() {
    console.log(this.b);
    b = "baby";
    console.log(b);
  }
  bottom();
}
fighting();
console.log(b);

 答案及解析:

 

var b = "boy";
console.log(b); //b已经声明并且赋值,输出boy
function fighting() {
  console.log(a); //函数内部用var声明了a,输出undefined
  console.log(c); //函数内部用var声明了c,输出undefined
  if (a === "apple") {
    a = "Alice";
  } else {
    //此时a=undefined,所以代码走else逻辑
    a = "Ada"; //a="Ada"
  }
  console.log(a); //输出Ada
  var a = "Andy"; //a=Andy
  middle();
  function middle() {
    console.log(c++); //middle函数内部用var声明了c,c=undefined c++=NaN
    var c = 100; //c=100
    console.log(++c); //c=101,输出101
    small();
    function small() {
      console.log(a); //small函数内部没有a,沿着作用域找到fighting函数内部的a=Andy,输出Andy
    }
  }
  var c = (a = 88); //a=88 c=88
  function bottom() {
    console.log(this.b); //bottom作为普通函数执行,this执行全局作用域
    //全局window对象没有b属性,只要b变量。因此通过window.b访问不到 输出undefined,不会报错
    b = "baby"; //赋值找b,b是全局变量,修改b=baby
    console.log(b); //输出baby
  }
  bottom();
}
fighting();
console.log(b); //全局变量被修改,输出baby

;