深入理解this指针和作用域

234 阅读19分钟

@[toc]

一、JavaScript中的原型和原型链

概要:在JavaScript中,原型和原型链是两个重要的概念,对于理解JavaScript的继承机制非常关键。在本文中,我们将深入探讨这两个概念,帮助你更好地理解它们在实际开发中的作用

1.1 构造函数创建对象

我们先使用构造函数创建一个对象:

function Person() {

}
var person = new Person();
person.name = 'zhangsang';
console.log(person.name) // zhangsang

在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person

1.2 prototype

每个函数都有一个 prototype 属性,比如:

function Course() {

}
Course.prototype.class = "Computer Science"; // prototype是函数才会有的属性

const course1 = new Course();
const course2 = new Course();

console.log(course1.class); // Computer Science
console.log(course1.class); // Computer Science

那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗? 其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 course1 和 course2 的原型。 那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。 用一张图表示构造函数和实例原型之间的关系

在这里插入图片描述 这里用 Object.prototype 表示实例原型。 那么该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢?

1.3 proto

这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

function Person() {

}
var person = new Person();

console.log(person.__proto__ === Person.prototype); // true

在这里插入图片描述 提问:既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

1,4 constructor

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数是有的:constructor,每个原型都有一个 constructor 属性指向关联的构造函数

function Person() {

}
console.log(Person === Person.prototype.constructor); // true

在这里插入图片描述 所以,这里可以得到:

function Person() {

}

var person = new Person();

console.log(person.__proto__ == Person.prototype) // true

console.log(Person.prototype.constructor == Person) // true

console.log(Object.getPrototypeOf(person) === Person.prototype) // true

在这里插入图片描述

1.5 实例与原型

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

例如:

function Course() {

}
Course.prototype.class = "Computer Science";

const course = new Course();

course.class = "Mathematics"

console.log(course.class); // Mathematics

delete course.class;

console.log(course.class); // Computer Science

在这里插入图片描述

在这个例子中,我们给实例对象 course添加了 class属性,当我们打印 course.class的时候,结果自然为 Mathematics。 但是当我们删除了 course 的 class 属性时,读取 course.class,从 Course 对象中找不到 class 属性就会从 course 的原型也就是 course.proto ,也就是 Course.prototype中查找,结果为 Computer Science。

  • 原型的原型
var obj = new Object();
obj.name = 'zhangsang'
console.log(obj.name) // zhangsang

其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图:

在这里插入图片描述

1.6 原型链

那 Object.prototype 的原型呢?

console.log(Object.prototype.__proto__ === null) // true

然而 null 究竟代表了什么呢? null 表示“没有对象”,即该处不应该有值。所以 Object.prototype.__proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。 所以查找属性的时候查到 Object.prototype 就可以停止查找了。 最后一张关系图也可以更新为:

在这里插入图片描述其中,红色为原型链

constructor

首先是 constructor 属性:

function Course() {

}
var course = new Course();

console.log(couse.constructor === Course); // true

当获取 course.constructor 时,其实 course 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 course 的原型也就是 Course.prototype 中读取,正好原型中有该属性,所以:

course.constructor === Course.prototype.constructor

proto

绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

继承

关于继承,前面提到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是: 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

二、词法作用域和动态作用域

2.1 作用域

作用域是指程序源代码中定义变量的区域。 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。 JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

2.2静态作用域和动态作用域

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。 而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var num = 1

function fn1() {
    console.log(num);
}

function fn2() {
    var num = 2
    fn1()
}

fn2()

假设JavaScript采用静态作用域,让我们分析下执行过程: 执行fn1()函数,先从fn1函数内部查找是否有局部变量num,,如果没有,就根据书写的位置,查找上面一层的代码,也就是 num 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程: 执行 fn1函数,依然是从 fn1函数内部查找是否有局部变量 num。如果没有,就从调用函数的作用域,也就是 fn2函数内部查找 num变量,所以结果会打印 2。JavaScript采用的是静态作用域,所以这个例子的结果是 1。

在这里插入图片描述

2.3 思考

// case 1
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope(); // local scope

// case 2
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()(); // local scope

两段代码各自的执行结果是多少?

local scope 因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。 而引用《JavaScript权威指南》的回答就是: JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。 但是在这里真正想探讨的是: 虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

三、执行上下文

3.1 顺序执行

写过 JavaScript 的开发者都会有个直观的印象,那就是顺序执行:

在这里插入图片描述 那这段呢? 在这里插入图片描述 打印的结果却是两个 foo2。 这是因为 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个准备工作,那这个一段一段中的段究竟是怎么划分的呢? 到底JavaScript引擎遇到一段怎样的代码时才会做准备工作呢?

console.log(add2(1,1)); //输出2
function add2(a,b){
    return a+b;
}
console.log(add1(1,1));  //报错:add1 is not a function
var add1 = function(a,b){
    return a+b;
}


用函数语句创建的函数add2,函数名称和函数体均被提前,在声明它之前就使用它。 但是使用var表达式定义函数add1,只有变量声明提前了,变量初始化代码仍然在原来的位置,没法提前执行。

3.2 可执行代码

这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了? 其实很简单,就三种,全局代码、函数代码、eval代码。 举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

3.3执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文 为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:

ECStack = [
    globalContext
];

当JavaScript 遇到下面的这段代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:

// 伪代码

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);

// fun2还调用了fun3!
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

3.4 回顾上文

在这里插入图片描述 两段代码执行的结果一样,但是两段代码究竟有哪些不同呢? 答案就是执行上下文栈的变化不一样。

模拟第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

模拟第二段:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

四、变量对象

4.1基础

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

4.2 变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。 因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

4.3 全局上下文

  1. 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
  2. 在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
  3. 例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

简单点说: 1、可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。

console.log(this);

2、全局对象是由 Object 构造函数实例化的一个对象

console.log(this instanceof Object);

3、 预定义的属性是否可用

console.log(Math.random());
console.log(this.Math.random());

4、作为全局变量的宿主

var a = 1;
console.log(this.a);

5、客户端 JavaScript 中,全局对象有 window 属性指向自身

var a = 1;
console.log(window.a);

this.window.b = 2;
console.log(this.b);

综上,对JS而言,全局上下文中的变量对象就是全局对象。

4.4 函数上下文

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。 活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

4.5 执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文;
  2. 代码执行;

4.5.1 进入执行上下文

当进入执行上下文时,这时候还没有执行代码, 变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)
  • 由名称和对应值组成的一个变量对象的属性被创建;
  • 没有实参,属性值设为 undefined;
  1. 函数声明
  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  1. 变量声明
  • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性; 举个例子:
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

在进入执行上下文后,这时候的 AO 是: 在这里插入图片描述

4.5.2 代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值 还是上面的例子,当代码执行完后,这时候的 AO 是:

在这里插入图片描述 总结:

  1. 全局上下文的变量对象初始化是全局对象;
  2. 函数上下文的变量对象初始化只包括 Arguments 对象;
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
  4. 在代码执行阶段,会再次修改变量对象的属性值;

4.5.3 思考题

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

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

第一段会报错:Uncaught ReferenceError: a is not defined。 第二段会打印:1。 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。 第一段执行 console 的时候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。 当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

demo2:

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

会打印函数,而不是 undefined 。 这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性.

五、作用域链

5.1 作用域链

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

5.2 函数创建

上文的词法作用域与动态作用域中讲到,函数的作用域在函数定义的时候就决定了。 这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个例子:

 
function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:


foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

5.3 函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。 这时候执行上下文的作用域链,我们命名为 Scope:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

5.4 总结

结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

在这里插入图片描述

六、this

6.1 Types

Types are further subclassified into ECMAScript language types and specification types. An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object. A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.

我们简单的翻译一下:

ECMAScript 的类型分为语言类型和规范类型。 ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。 而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。 我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。 这里要了解的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。

6.2 Reference

那什么又是 Reference ? The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators. 所以 Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。 这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。 再看接下来的这段具体介绍 Reference 的内容:

A Reference is a resolved name binding. A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

这段讲述了 Reference 的构成,由三个组成部分,分别是:

  • base value;
  • referenced name;
  • strict reference; 可是这些到底是什么呢?我们简单的理解的话: base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。 referenced name 就是属性的名称。

举个例子:

var foo = 1;

// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};
var foo = {
    bar: function () {
        return this;
    }
};
 
foo.bar(); // foo

// bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBase 和IsPropertyReference。

6.2.1 GetBase

GetBase(V). Returns the base value component of the reference V. 返回 reference 的 base value。

var foo = 1;

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

GetValue(fooReference) // 1;

GetValue 返回对象属性真正的值,但是,调用 GetValue,返回的将是具体的值,而不再是一个 Reference

6.2.2 IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false. 如果 base value 是一个对象,就返回true。

6.3 如何确定this的值

  • Let ref be the result of evaluating MemberExpression;
  • if Type(ref) is Reference, then
    • If IsPropertyReference(ref) is true, then
    • Let thisValue be GetBase(ref).
  • Else, the base of ref is an Environment Record
    • Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
  • Else, Type(ref) is not Reference.
  • Let thisValue be undefined. 让我们描述一下:
  1. 计算 MemberExpression 的结果赋值给 ref;
  2. 判断 ref 是不是一个 Reference 类型;
  3. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
  4. 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
  5. 如果 ref 不是 Reference,那么 this 的值为 undefined;

6.4 具体分析

6.4.1 计算 MemberExpression 的结果赋值给 ref

什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions:

MemberExpression :

  • PrimaryExpression // 原始表达式
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

举个例子:

function foo() {
    console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {
    return function() {
        console.log(this)
    }
}

foo()(); // MemberExpression 是 foo()

var foo = {
    bar: function () {
        return this;
    }
}

foo.bar(); // MemberExpression 是 foo.bar

所以简单理解 MemberExpression 其实就是()左边的部分。

6.4.2 判断 ref 是不是一个 Reference 类型。

关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());

6.4.3 foo.bar()

上面的demo种,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢? 根据规范,这里展示了一个计算的过程,什么都不管了,就看最后一步: Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict. 我们得知该表达式返回了一个 Reference 类型 根据之前的内容,我们知道该值为:

var Reference = {   base: foo,   name: 'bar',   strict: false };
  1. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢? 前面我们说了IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。 base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。 这个时候我们就可以确定 this 的值:
     this = GetBase(ref),

GetBase 也已经铺垫了,获得 base value 值,这个例子中就是fo,所以 this 的值就是 foo ,示例1的结果就是 2。

6.4.4 (foo.bar)()

console.log((foo.bar)());

foo.bar 被 () 包住 Return the result of evaluating Expression. This may be of type Reference. NOTE This algorithm does not apply GetValue to the result of evaluating Expression. 实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。

6.4.5 (foo.bar = foo.bar)()

看示例3,有赋值操作符, 因为使用了 GetValue,所以返回的值不是 Reference 类型, 按照之前讲的判断逻辑,如果 ref 不是Reference,那么 this 的值为 undefined this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。

同理,示例4和示例5因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined。

6.4.6 总结


var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

注意:以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。

七、执行上下文

还是以之前的代码为例

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

执行过程如下: 在这里插入图片描述