作用域、作用域链、闭包

一、作用域

作用域,是指变量的生命周期。

  • 全局作用域
  • 函数作用域
  • 块级作用域
  • 词法作用域
  • 动态作用域

1、全局作用域

全局变量:生命周期将存在于整个程序之内。能被程序中任何函数或者方法访问。在javascript内默认是可以被修改的。

1.1显示声明

带有关键字var的声明:

var a = 1;
var f = function(){
    console.log("come on");
};
//全局变量会挂在到window对象上
console.log(window.a);
console.log(window.f);复制代码


1.2隐式声明

function f(num){
    result = num + 1;
    return result;
}
f(1);
console.log(window.result);复制代码


不带有var关键字的变量result,也被隐式声明为一个全局变量。挂载在window对象上。

2、函数作用域

function f(){
    var a = 1;
}
console.log(a);复制代码


2.1如何访问函数作用域内的变量呢?

法一:通过return返问函数内部变量

function f(num){
    var result = num++;
    return result;
}
console.log(f(1));复制代码


function f(num){
    var result = num + 1;
    return result;
}
console.log(f(1));复制代码


function f(num){
    var result = ++num;
    return result;
}
console.log(f(1));复制代码


以上三段程序也体现了i++和++i的区别。

法二、通过闭包访问函数内部的变量

function outer(){
    var value = 'inner';
    return function inner(){
        return value;
    }
}
console.log(outer()());复制代码


2.2立即执行函数

立即执行函数能够自动执行(function(){})()里面包裹的内容,能够很好地消除全局变量的影响。

(function(){
    var a = 1;
    var foo = function(){
        console.log("haha");
    }
})();
console.log(window.a);
console.log(window.foo);复制代码


3、块级作用域

在 ES6 之前,是没有块级作用域的概念的。

for(var i = 0; i < 3; i++){

}
console.log(i);   复制代码


很明显,用 var 关键字声明的变量,存在变量提升,相当于:

var i;
for(i = 0; i < 3; i++){

}
console.log(i);   复制代码

如果需要实现块级作用域,可以使用let关键字,let关键字是不存在变量提升的。

for(let i = 0; i < 3; i++){

}
console.log(i);
复制代码


同样能形成块级作用域的还有const关键字。

if(true){
    const a = 1;
}
console.log(a);复制代码


块级作用域的作用以及常考的面试题

for(var i = 0; i < 3; i++){
    setTimeout(function(){
        console.log(i);
    },200);
}复制代码


为什么i是3呢?

原因因为var声明的变量可以进行变量提升,i是在全局作用域里面的,for()循环是同步函数,setTimeout是异步操作,异步操作必须等到所有的同步操作执行完毕后才能执行,执行异步操作之前i已经是3,所以之后会输出同一个值3。

如何让它按我们想要的结果输出呢?

法一:最简单使用let

for(let i = 0; i < 3; i++){
    setTimeout(function(){
        console.log(i);
    },200);
}
复制代码


法二:调用函数,创建函数作用域;

for(var i = 0; i < 3; i++){
    f(i);
}
function f(i){
    setTimeout(function(){
        console.log(i);
    },200);
}复制代码


法三、立即执行函数

for(var i = 0; i < 3; i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        },200) 
    })(i);
}复制代码


立即执行函数同函数调用,先把for循环中的i记录下来,然后把i赋值给j,然后输出0,1,2。

4、词法作用域

函数的作用域在函数定义的时候就决定了。

var value = 'outer';
function foo(){
    var value = 'middle';
    console.log(value);   //middle
    function bar(){
        var value = 'inner';
        console.log(value);  //innner
    }
    return bar();
}
foo();
console.log(value);   //outer复制代码

当我们要使用声明的变量时:JS引擎总会从最近的一个域,向外层域查找;


例:面试题

var a = 2;
function foo(){
    console.log(a);
}
function bar(){
    var a = 3;
    foo();
}
bar();复制代码


如果是词法作用域,也就是现在的javascript环境。变量a首先会在foo()函数里面查找,如果没有找到,会根据书写的位置,查找上一层的代码,在这里是全局作用域,找到并赋值为2,所以控制台输出2。

我们说过,词法作用域是写代码的时候就静态确定下来的。Javascript中的作用域就是词法作用域(事实上大部分语言都是基于词法作用域的),所以这段代码在浏览器中运行的结果是输出 2

作用域的"遮蔽"

作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符。

var a = 0;
function test(){
    var a = 1;
    console.log(a);//1
}
test();复制代码

var a = 0;
function test(){
    var a = 1;
    console.log(window.a);//0
}
test();复制代码

通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。

5、动态作用域

而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用

动态作用域,作用域是基于调用栈的,而不是代码中的作用域嵌套;

请听面试题:

var x = 3;
var y = 3;
function A(y){
    var x = 2;
    var num = 3;
    num++;    //4
    function B(num){
        return x * y * num;    //x,y,num:1,5,4
    }
    x = 1;
    return B;
}
console.log(A(5)(4));
复制代码


解析:

本题的关键在于确定x的值,函数B是在 return B;时执行的,所以x的值在函数调用前已经修改为1;所以返回20。

二、作用域链

每一个 javaScript 函数都表示为一个对象,更确切地说,是 Function 对象的一个实例。

Function 对象同其他对象一样,拥有可编程访问的属性。和一系列不能通过代码访问的属性,而这些属性是提供给 JavaScript 引擎存取的内部属性。其中一个属性是 [[Scope]] ,由 ECMA-262标准第三版定义。

内部属性 [[Scope]] 包含了一个函数被创建的作用域中对象的集合。

这个集合被称为函数的 作用域链,它能决定哪些数据能被访问到。

来源于:《 高性能JavaScript 》;

例:

function add(x,y){
    return x + y;
}
console.log(add.prototype);
复制代码


[[Scope]] 属性下是一个数组,里面保存了,作用域链,此时只有一个 global

理解词法作用域的原理

var a = 2;
function foo(){
    console.log(a);   //2
    console.log(foo.prototype);
}
function bar(){
    var a = 3;
    console.log(a);   //3
    foo();
    console.log(bar.prototype);
}
bar();复制代码

node环境下:



浏览器下:



疑惑:为什么在node中scopes数组中有两个对象,在浏览器中scopes数组中只有一个对象。

原因:node的模块化,本质上也是在外层添加一个匿名函数,因为node的模块化,在编译的时候,给每个JS文件外部包裹一个匿名函数。所以会出现scopes中有两个对象。展开scopes[0],会发现里面确实包含在浏览器中是全局的变量或全局的函数,而在node环境下由于多包裹了一层匿名函数,会让它存在于closure中。

var a = 2;
function bar(){
    var a = 3;
    console.log(a);   //3
    foo();
    console.log(bar.prototype);
    function foo(){
        console.log(a);   //3
        console.log(foo.prototype);
    }
}
bar();复制代码

node环境下:



浏览器下:



全局作用域链是在全局执行上下文初始化时就已经确定了。

证明:

console.log(add.prototype);     //1声明前
function add(x,y){
    console.log(add.prototype);  //2运行时
    return x + y;
}
add(1,2);
console.log(add.prototype);   //3执行后
复制代码

1声明前


2运行时


3执行后


作用域链是在 JS 引擎完成初始化执行上下文环境就已经确定了。

理解作用域链的好处:如果作用域链越深, [0] => [1] => [2] => [...] => [n],我们调用的是全局变量,它永远在最后一个(这里是第 n 个),这样的查找到我们需要的变量会引发多大的性能问题,所以,尽量将 全局变量局部化 ,避免作用域链的层层嵌套。

理解执行上下文


  • 在函数未调用之前,add 函数的[[Scope]]属性的作用域链里面已经有以上内容。
  • 当执行此函数时,会建立一个称为 执行上下文 (execution context) 的内部对象。一个 执行上下文 定义了一个函数执行时的环境,每次调用函数,就会创建一个 执行上下文 ;一旦初始化 执行上下文 成功,就会创建一个 活动对象 ,里面会产生 this arguments 以及我们声明的变量,这个例子里面是 xy
建立执行上下文阶段


结束执行上下文阶段


作用域链和执行上下文的关系

作用域链本质上是指向一个指针,指向变量对象列表。创建函数时,会把全局变量对象的作用域链添加在[[Scope]]属性中。调用函数时,会为函数创建一个执行环境的作用域链,并且创建一个活动对象,并将其推入执行环境作用域链的前端。函数局部环境的变量对象只有在函数执行的过程中才存在。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包的情况有所不同。

三、闭包

function createComparisonFunction(propertyName){
    return function(object1,object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
}
//创建函数
var compareNames = createComparisonFunction('name');
//调用函数
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除对匿名函数的引用
compareNames = null;复制代码

调用compareNames()函数的过程中产生的作用域链之间的关系图如下


createComparisonFunction执行完毕后,其执行环境的作用域链会被销毁,但其活动对象不会被销毁,因为匿名函数的作用域链还在引用这个活动对象。直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

闭包与变量

function createFunctions(){
    var result = new Array();
    for(var i = 0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
console.log(createFunctions()[0]());   //10复制代码

实际上每个函数都会返回10。原因:因为每个函数的作用域链中都会保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量。

如何让闭包输出想要的结果呢?

function createFunctions(){
    var result = new Array();
    for(var i = 0; i < 10; i++){
        result[i] = function(num){
            return function(){
                return num;
            }
        }(i);
    }
    return result;
}
for(var j = 0; j < 10; j++){
    console.log(createFunctions()[j]());
}复制代码


理解:我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,然后给该匿名函数传入参数,由于参数是按值传递的,所以相当于把当前的i赋值给num,再在该匿名函数内生成一个闭包,返回num。

参考:理解 JS 作用域链与执行上下文





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值