作用域、作用域链、闭包

一、作用域

A、定义

代码在运行时,各个变量、函数和对象的可访问性。换句话说,作用域决定了你的代码里的变量和其他资源各个区域中的可见性

B、分类

1、 全局作用域 (在函数之外的变量)

当你在文档中(document)编写 JavaScript 时,你就已经在全局作用域中了。JavaScript 文档中(document)只有一个全局作用域。定义在函数之外的变量会被保存在全局作用域中

var name = 'hello world'; 
//默认是全局的

全局作用域里的变量能够在其他作用域中被访问和修改。

var name = 'hello world';
console.log(name); // 'hello world'
 
function fn() {
    console.log(name); // name 可以在这里和其他地方访问
} 
fn(); // 'hello world'

2、 局部作用域 (在一个函数中的变量)

在函数中的变量就在局部作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

//全局作用域
function someFunction() {
    // 局部作用域
    function someOtherFunction() {
        // 局部作用域
    }
}
 
//全局作用域
function anotherFunction() {
    // 局部作用域
}
//全局作用域

注:每个函数在调用的时候会创建一个新的作用域。

C、块语句

块级声明包括if和switch,以及for和while循环,和函数不同,它们不会创建新的作用域。在块级声明中定义的变量从属于该块所在的作用域

if (true) {// 这个 if 条件块不创建新的作用域
    var name = 'hello world'; // name 仍在全局范围中
}
console.log(name); // 'hello world'

ECMAScript 6 引入了let和const关键字。这些关键字可以代替var

var name = 'hello world';
let likes = 'Coding';
const skills = 'Javascript and PHP';

和var关键字不同,letconst关键字支持在块级声明中创建使用局部作用域

if (true) {// 这个 if 条件块不创建新的作用域
    var name = 'hello world';// 因为 var 关键字,name 位于全局范围中
    
    let likes = 'Coding'; 
    // 因为 let 关键字,likes在本地范围中,相当于是局部变量
   
    const skills = 'JavaScript and PHP'; 
    // 因为 const 关键字,skills 在本地范围中,相当于是局部变量
}
 
console.log(name); // 'hello world'
console.log(likes); 
// 报错了,显示 Uncaught ReferenceError: likes is not defined
// 因为是likes 是局部变量,没法读取到
console.log(skills); 
// 报错了,显示 Uncaught ReferenceError: skills is not defined
// 因为是skills 是局部变量,没法读取到

总结

(1) 一个应用中,全局作用域的生存周期与该应用相同。

(2) 局部作用域只在该函数调用执行期间存在。

二、作用域链

A、定义

每当声明一个函数的时候会有一个独立的的作用域,遇到函数执行需要调用某一个变量的时候,需要先在当前函数的作用域下寻找。如果当前函数的作用域下找不到该值变量,就需要进入到创建该函数所在的作用域下寻找,以此往上直至找到为止。这样就形成了一条作用域链。

B、实例

var a = 1
function fn1(){
    function fn2(){
        console.log(a)
    }
    function fn3(){
        var a = 4
        fn2()
    }
    var a = 2
    return fn3
}
var fn = fn1()
fn() // 2
分析:

调用fn1()后,返回fn3,而fn3中又调用了fn2(),而fn2输出a,但是fn2中没有定义a,所以在上级作用域找a,即fn1的作用域,所以输出2

var a = 1
function fn1(){
    function fn3(){
        var a = 4
        fn2()
    }
    var a = 2
    return fn3
}
function fn2(){
    console.log(a)
}
var fn = fn1()
fn() // 1
分析:

调用fn1之后,return fn3,而fn3调用了fn2,fn2输出a,而fn2中没有a,所以在fn2的上级作用域中找a,所以输出全局作用域的a,即1

var a = 1
function fn1(){
  function fn3(){
    function fn2(){
      console.log(a)
    }
    fn2()
    var a = 4
  }
  var a = 2
  return fn3
}
var fn = fn1()
fn() 
//undefined 
分析:

因为变量提升,调用fn2时,在fn3的作用域下,a被声明了,但是先console.log(a),但没来得急赋值

总结

(1) 函数在执行的过程中,先从自己内部找变量

(2) 如果找不到,再从创建当前函数所在的作用域(词法作用域)去找, 以此往上

(3) 注意找的是变量的当前的状态

三、闭包

A、定义

一个函数连同它的词法作用域所在的,使用的这个变量的集合

B、作用

(1) 封装数据

(2) 暂存数据

C、经典的闭包实例

function car(){
  var speed = 0 
  function fn(){
    speed++
    console.log(speed)
  }
  return fn
}

var speedUp = car() 
speedUp()   //1
speedUp()   //2

分析:

从上图有看到闭包就是有一个函数car ,函数里有一个变量speed,函数内部又声明了一个函数fn,然后return fn

Why?

(1) 假设函数car 里面没有fn 函数这一堆东西,当我执行 var speedUp = car(),执行完成之后,那函数car内部的局部变量就会消毁掉,那么speed 就没了,就不存在了

(2) 现在我们在函数car 内部声明了fn 函数,把这个函数返回出来return fn,赋值给speedUP,这个时候,因为我们的全局作用域是一直存在的,除非把当前页面给关掉,它才被消毁,所以的话,它一直存在的话,这个speedUp 就是一直存在的

(3) 这个speedUP是指什么呢,就是指这个fn,也就是car函数内部声明的这个fn函数

那这个fn函数就是一直存在,换句话说,这个car 函数作用域内的东西就无法被销毁,也就是变量speed和fn函数无法销毁,因为它里面有东西被人用,其实是和浏览器的垃圾回收机制是相关的

(4) 当我们去执行speedUp()时,实际上就是去执行fn函数,然后fn函数内部是没有声明这个speed变量的,它会从上一级的词法作用域去找,也就是这个函数fn,声明时所在的作用域,也就是car函数内部, 最后找到这个speed,然后把这个值speed++ ,即这个speed 值加1,然后再次调用这个函数,所以又在之前的值基础上,再加1

换句话说,正常情况下,car 执行完后,speed变量就消失了,就没有了,我们也无法去获取,但是现在在函数car内部,又返回出一个函数fn,然后函数fn内部又使用这个speed, 就导致创建了一个闭包

那什么是个闭包呢

就是这个函数fn连同它词法作用域下所在的使用的这个变量speed的集合。也就是变量speed 和 函数fn 加在一起,就是一个闭包

闭包有什么用呢?我们直观的看一看

(1) 当我调用speedUp()时,fn函数内的speed 的值会加1,换句话说这个变量是不是就暂存下来了,我们无法去直接访问这个变量speed,但是我们可以去操作它,而且呢,它会暂存起来,不会被销毁掉

(2) 第二个就是封装数据,就是把speed 给封装起来

注:如果不执行return fn的话,那car函数 执行完之后,fn函数内的所有的局部变量都会销毁掉,销毁掉就什么都没有了,什么都没有了就不存在有没有闭包了

D、其他实例

理解了下面几个实例,你就能熟练理解运用闭包了

例1 如下代码输出多少

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

分析:

有一个空数组 fnArr,然后我们去遍历这个数组,遍历时侯给这个数组去赋值,数组的每一个值是对应匿名函数,然后里面有一个 return i,最后是console.log( fnArr[3] () ) ,其中 fnArr[3] () 是立即执行表达式, 相当于会立即执行这个函数, 得到最终的值

是3吗,不是,为什么呢?

(1) 因为我们在做遍历的时侯,就只有去赋值了,没有去执行return i 的,那这就是一个函数,相当于fnArr里面对应的这么几个函数名

(2) 当去执行 fnArr[3] () 的时候,就会去执行对应的函数,然后return i,此时i 是多少呢?

那这个函数内部没有i ,那从哪去找呢?

从这个函数创建它的词法作用域去找, 它的词法作用域,说白了,不就是我们的全局作用域吗

因为 for 循环,并不是一个函数,所以这里面没有作用域,自然for 下面的function 词法作用域是全局作用域

那全局作用域里面的 i 是多少呢

此时这里面因为已经执行完了,for循环完成之后 ,这个 i 变成 10 啦

所以最终找到的 i 就是 10 ,输出结果是 10

从控制台这里,也能看出源由啦

image.png

图中可以看出 fnArr ,对应是一个数组,数组里面都是函数,函数里面有一个Scopes , 对应的是什么呢?
对应的是Global全局作用域,为什么呢? 前面我们说了作用域链

(1) 当我们去使用一个变量时侯,会从函数内部去找,也就是它的函数内部的一个活动对象

(2) 如果找不到的话,再从它的词法作用域去找。它的词法作用域是什么呢?
就是这个函数所创建它的所得的作用域

总结:

(1) 打开fnArr函数,会看到Scopes
其实这个函数在执行过程中,先从自已里面找,找不到的话,再从Scopes里面找,
它的Scopes 是谁呢,Scopes就是Global

(2) 如果嵌套几层的话,它的Scopes下面 还有一个,那其实这就是它的一个作用域链

所以当我们执行fnArr[3] 的时侯,从内部找 i , 找不到的话,再从Scopes里面找, 从 window里找 ,对不对,执行它 fnArr[3] ,得到是10,因为i 最终是 10

如果要输出3 的话,可以这么改

方法1

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

(1) 首先 fnArr[3] () 输出什么,哎呀,我有点看不懂。那我们来变装下吧!
那假设这个for 循环没有10层,只有2层, 代码变成了

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

这样子的话,只有2层,我们的for 循环也可以去掉了,相当于执行2次,代码变成了

fnArr = []
fnArr[0] =  (function(j){
    return function(){
      return j
    } 
  })(0)
fnArr[1] =  (function(j){
    return function(){
      return j
    } 
  })(1)
fnArr[1]()

一个数组里面有2个值,那我还不如直接写2个值,代码变成了

var a =  (function(j){
    return function(){
      return j
    } 
  })(0)
var b =  (function(j){
    return function(){
      return j
    } 
  })(1) 
b()

这个时候我问你,b执行的结果是多少?
从上面代码看,b 等于 一个立即执行函数表达式,哈哈,它又相当于什么呢,

那即然它是匿名函数,我可以给它加一个函数名fn2, 然后给a也加一个函数名fn1 ,这下代码变成了

var a =  (function fn1(j){
    return function(){
      return j
    } 
  })(0)
var b =  (function fn2(j){
    return function(){
      return j
    } 
  })(1) //声明了一个函数,然后去执行它
b()

如果这样子的话,我还不如这样子改

function fn1(j){
    return function(){
      return j
    } 
}
function fn2(j){
    return function(){
      return j
    } 
}
var a = fn1(0)
var b = fn2(1) // 相当于j 等于1
a()
b()

fn2(1)去执行这个函数的结果,相当于j 等于1 ,也就是说执行这个函数的结果,就是return了一个函数,换句话说 b 的值,就是这里面的函数

当去执行b的时侯,也就是当去执行这个函数的时候 ,return j ,j 是多少呢?
那j 在哪里呢?是不是在函数内部 b 里面 ,函数内部没有j ,没有j的话,就从哪里找呢
就从这个函数
function (){
​ return j
}
所在的词法作用域去找,也就是它声明的地方(fn2函数内部),那代码可以变成

function fn1(j){
    return function(){
      return j
    } 
}
function fn2(j){
    function f(){
      return j
    } 
    return f
}
var a = fn1(0)
var b = fn2(1)
a()
b()

这个时侯, b 就等于 f ,那执行b的时候, return j,j 在哪里呢,函数f里面没有,那它会去它的词法作用域去找,即fn2这个函数内部去找,这个内部有没j 呢,有,因为在调用fn2时,传递了一个j ,相当于
var j = arguments[0], 也就是1 , 所以输出了1

上面部分代码变成了,其他没变

function fn2(){
  var j = arguments[0]  
    function f(){
      return j
    } 
    return f
}
var b = fn2(1)
b()

方法2

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

还记得那个经典闭包实例吧,我们可以演变一下

//改前

function car(){
  var speed = 0 
  function fn(){
    speed++
    console.log(speed)
  }
  return fn
}
var speedUp = car()

//改后

function car(){
  var speed = 0 
  return function (){
    speed++
    console.log(speed)
  }
}
var speedUp = car() 
分析:

假设car 有一个形参 speed ,那是不是代码变成了

function car(){//arguments 对象可以在函数体内部读取所有参数
  var speed = arguments[0] //arguments[0]就是第一个参数speed
  return function (){
    speed++
    console.log(speed)
  }
}
var speedUp = car(3) 

那现在是声明了一个函数car,去执行它car(3)
那这样子,我可以把一个函数写过来,代码变成了

var speedUp = (function car(){
  var speed = arguments[0] 
  return function (){
    speed++
    console.log(speed)
  }
})(3)

即然这样子写的话,那个函数名car 也可以去掉,那代码变成了

var speedUp = (function(){
  var speed = arguments[0] 
  return function (){
    speed++
    console.log(speed)
  }
})(3)

现在不就变成了一个立即执行函数表达式了
所以说立即执行表达式里面如果有return的话,那就是生成了闭包

和刚刚最原始的闭包,经过一个个的演化,替换,得到的效果是一模一样的

和上面的这个

var b =  (function fn2(j){
    return function(){
      return j
    } 
 })(1)

是不是一样的

哎呀,累死我啦!还没完!

回到最上面的这段代码

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

按照这种方式去演变,这里

fnArr[i] =  (function(j){
    return function(){
      return j
    } 
  }) 

就生成了10个闭包,这里面有10个函数,然后每个闭包里暂存了一个变量,代码变成了

fnArr[i] =  (function(j){
    var j = arguments[0] //暂存的变量就在这里
    return function(){
      return j
    } 
})

也就是说在一开始的时侯,传递的 0,1,2,...9,然后这里面有10 个闭包,存了10个不一样的值,所以你下次去执行这个函数的时侯, 执行对应的函数的时候,它会从上面去找 j ,也就找到自已对应的词法作用域下面的这个值 ,也就是这里面的一个临时变量 var j = arguments[0]

例2 封装一个 Car 对象

通过接口去操作一些数据,不能直接访问这些数据,为了数据有一定的安全性

var car = (function(){
   var speed = 0;
   function set(s){
       speed = s
   }
   function get(){
      return speed
   }
   function speedUp(){
      speed++
   }
   function speedDown(){
      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
分析:

car是一个立刻执行的函数表达式中return出来的结果。

(1) return出来是一个对象,有四个属性 set、get、speedUp、speedDown,四个属性对应的是一个值,即四个函数set、get、speedUp、speedDown。这四个函数就用于操作speed的值。

(2) 为什么得不到释放呢?

a、这导致了car永远得不到释放,因为它是我们的全局变量

b、car 得不到释放,那return对象也得不到释放,因为这个对象是引用类型,它们本质上是同一个东西

c、return对象得不到释放,那里面的属性值也没法释放

d、属性值得不到释放,对应的函数也无法释放,那speed变量自然也释放不了,就生成了一个闭包

例3 如下代码输出多少?如何连续输出 0,1,2,3,4
for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log('delayer:' + i )
  }, 0)
}// 连续输出5个 delayer:5
分析:

setTimeout 会把当前的执行(也就是函数的代码)添加到任务队列里面,相当于设置了5个计时器,并没有开始,当5个设置完成之后,这个时侯 i 已经变成 5 啦

此刻才去执行任务队列里面的代码,然后一个个去执行setTimeout 里面的函数 ,当执行它的时候,开始找 i 的值,for循环已经执行完了,i 已经变成5 啦,所以连续输出5个5

改后:

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

我们可以简化下代码,for 循环 变成 2 时,代码变成了

function fn1(){
    var j = arguments[0]
    setTimeout(function(){
      console.log('delayer:' + j )
    }, 0) 
}
fn1(0)

function fn2(){
    var j = arguments[0]
    setTimeout(function(){
      console.log('delayer:' + j )
    }, 0) 
}
fn2(1)

通过一个立刻执行的函数表达式,生成一个闭包。因为for循环不会产生一个作用域,所以可以不用return。当然用return也可以

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

相当于 setTimeout 第一层带参数 j 函数会立即去执行,并没有延时,执行过程中,传递了 i , 其实得到了return 出来的函数结果, setTimeout 能用这个函数,用的过程中,如果用到这个变量 j,就相当于那个中间夹了一个临时变量var j = arguments[0],存进去了,那下次要用过这个变量j ,它就会从这里面找。

也就是说把 i = 0 ,变成 j 放到那里, i = 1,变成 j 放到那里 ,依次类推,到4

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

例4

function makeCounter() {
  var count = 0

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

var counter = makeCounter()
// 相当于 return 函数 返回的值 赋值给 counter
var counter2 = makeCounter();
// 相当于把第二次执行 return 函数 返回的值给 counter 也就是把第一次执行的结果 count +1
console.log( counter() ) // 0
console.log( counter() ) // 1

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


分析:

当我们第一次执行 makeCounter()时,相当于有了作用域 ,有了一个活动对象count,再次执行makeCounter,又有了一个新的作用域,有一个新的活动对象count, 所以呀,它们是独立的。切记!

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

var users = [
  { name: "John", age: 20, company: "Baidu" },
  { name: "Pete", age: 18, company: "Alibaba" },
  { name: "Ann", age: 19, company: "Tecent" }
]

users.sort(byName) 
users.sort(byAge)
users.sort(byField('company'))

补全后代码变成

function byName(user1, user2){
  return user1.name > user2.name
}

function byAge (user1, user2){
  return user1.age > user2.age
}

function byFeild(field){
  return function(user1, user2){
    return user1[field] > user2[field]
  }
}
users.sort(byField('company'))

注:sort 排序 后面必须是一个函数,所以需要返回一个参数。

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

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

分析:

(1) sum(1) 后面跟着一个() ,表示一个没有执行的函数,其中sum(1) 是一个函数名,相当于

function sum(){
    return function(){
    }
}

(2) sum(1) (2) 表示sum(1) 传递了一个参数2 ,返回一个东西,也就是相当于

function sum(a){
    return function(b){
    }
}
sum(1)(2)

(3) 最后我们得到a+b的值

function sum(a) {
  return function(b) {
    return a + b
  }
}

总结:

函数柯里化-只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

可看看方方老师的文章 https://zhuanlan.zhihu.com/p/22486908

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343