做用域、做用域链、闭包

1、做用域

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

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

一、全局做用域前端

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

1.1显示声明node

带有关键字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对象上。数组

二、函数做用域浏览器

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


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

法一:经过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的区别。

法2、经过闭包访问函数内部的变量

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);复制代码


三、块级做用域

在 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);
}复制代码


法3、当即执行函数

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


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

四、词法做用域

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

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();复制代码

经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量若是被遮蔽了,不管如何都没法被访问到。

五、动态做用域

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

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

请听面试题:

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。

2、做用域链

每个 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]]属性中。调用函数时,会为函数建立一个执行环境的做用域链,而且建立一个活动对象,并将其推入执行环境做用域链的前端。函数局部环境的变量对象只有在函数执行的过程当中才存在。通常来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域。可是闭包的状况有所不一样。

3、闭包

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 做用域链与执行上下文

相关文章
相关标签/搜索