作用域链与闭包

1.作用域链

1.1 作用域链是什么?

作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。
首先,代码在其对应的环境中执行时,数据是保存在其变量对象中的。但是有时会使用其范围外的数据。当需要查找变量的值时,现将变量解析,然后从链表的第一项开始寻找,找到为止。

1.2 作用域链的形成

全局环境中,作用域链由一个全局对象组成。
而函数中,作用域链的形成分为两个过程
(1)函数创建时:会创建一个预先包含全局变量对象的作用域链,被保存在内部的【【scope】】属性中。
(2)当调用函数时,会为函数创建一个执行环境,然后通过复制函数的【【scope】】属性中的对象构建起执行环境的作用域链。然后又有一个活动对象被创建并推入执行作用域链的最前端。

即函数作用域链 = AO + scope

1.3 作用域链形成的实际列子

引用汤姆大叔的文章

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); // 60

全局上下文的变量对象是:

globalContext.VO === Global = {
x: 10
foo:
};

在“foo”创建时,“foo”的[[scope]]属性是:

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

在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

fooContext.AO = {
y: 20,
bar:
};

“foo”上下文的作用域链为:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];

内部函数“bar”创建时,其[[scope]]为:

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

在“bar”激活时,“bar”上下文的活动对象为:

barContext.AO = {
z: 30
};

“bar”上下文的作用域链为:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];

1.4 此过程中闭包的形成

函数(调用时),会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化函数的活动对象(AO),但在作用域链中,外部函数的活动对象始终处于第二位,外部的外部处于第三位,以此类推。
执行环境有一个表示变量的对象--变量对象(VO),全局环境的VO始终存在,函数这样的局部环境的VO只在函数的执行过程中存在。
内部函数中将会将外部函数的活动对象添加到其作用域链中,当外部函数执行完毕后,其执行环境的作用域链会被销毁,而活动对象会仍然保留在内存中。

由于作用域链的形成机制,导致了某些函数在执行完毕后,并不会销毁其活动对象,因为其被包含在了其他函数的作用域链中。

2. 闭包

可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。在ECMAScript中,所有的函数都是第一类对象。

在函数中,如果包含了自由变量(并未在函数内部声明),那么即产生了闭包。因为它涉及到了另外一个环境的数据,那么会将另外一个环境中的VO暂存起来。

2.2 闭包实战

2.2.1如下代码输出多少?如果想输出3,那如何改造代码?

var fnArr = [];
for (var i = 0; i < 10; i ++) {
  fnArr[i] =  function(){
    return i
  };
}
console.log( fnArr[3]() )

分析:代码的本意是想,fnArr3中,是调用的第数组中的四个函数,并且函数范围值与数组下标是一样的。这里会直接输出10,因为数组中函数仅仅是声明了,I并未赋值,等到调用时,其作用域中并不包含I,就向上级寻找I,而此时所有的函数都是共享一个父作用域,I的值为10.
如果要输出3,就是让每一次为数组赋值时,能够让函数能够记住此时i的值,并在调用的时候能够取到。
所以我们将循环更改一下:
for (var i = 0; i < 10; i ++) {
(function(j){
fnArr[j] = function(){
return j
};
})(i);
}
这里我们将赋值过程用一个立即执行函数封装了起来,并且通过参数传值,将i的值保存在了外层立即执行函数的执行上下文中。

2.2.2 封装一个 Car 对象

var Car = (function(){
    var speed = 0;
    function set(speed1){
        speed = speed1
        console.log(speed)
    }
    function get(){
        console.log(speed)
        return speed;
    }
    function speedUp(){
        speed++;
        console.log(speed)
    }
    function speedDown(){
        speed--;
        console.log(speed)
    }
    return {
        set: set,
        get: get,
        speedUp: speedUp,
        speedDown: speedDown
    }
})()
Car.set(30)
Car.get() //30
Car.speedUp()
Car.get() //31
Car.speedDown()
Car.get()  //30

这里用一个立即执行函数,并返回我们需求的对象的形式来封装。这样的好处是所有的数据都被封装在了立即执行函数中,不会暴露在外面。

2.2.3 如下代码输出多少?如何连续输出 0,1,2,3,4

for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log('delayer:' + i )
  }, 0)
}

分析:setTimeout外层创建一个闭包

for(var i=0; i<5; i++){
    (function (i){
    setTimeout(function(){
        console.log('delayer:' + i )
    }, 0)
    })(i)
}

2.2.4 如下代码输出多少?

function makeCounter() {
  var count = 0

  return function() {
    return count++
  };
}

var counter = makeCounter()
var counter2 = makeCounter();

console.log( counter() ) // 0
console.log( counter() ) // 1

console.log( counter2() ) // 0
console.log( counter2() ) // 1

分析:makeCounter都是返回了一个函数。且couter
与couter2所返回的函数是同名的。但是每次调用时,进入执行上下文,然后绑定了新的作用域链了。

2.2.5 补全代码,实现数组按姓名、年纪、任意字段排序

var users = [
    { name: "John", age: 20, company: "Baidu" },
    { name: "Pete", age: 18, company: "Alibaba" },
    { name: "Ann", age: 19, company: "Tecent" }
]
function byField(string){
    return function(user1,user2){
        return user1[string] >user2[string]
    }
}
users.sort(byField('age'))

2.2.6 写一个 sum 函数,实现如下调用方式

console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4

解答:

function sum(val1){
  return function(val2){
    return val1+val2;
    }
}

你可能感兴趣的:(作用域链与闭包)