一、作用域
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关键字不同,let和const关键字支持在块级声明中创建使用局部作用域。
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
从控制台这里,也能看出源由啦
图中可以看出 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