资讯专栏INFORMATION COLUMN

js函数、作用域和闭包

Lin_YT / 638人阅读

摘要:在函数内部定义的变量,外部无法读取,称为局部变量语言特有链式作用域结构,子对象会一级一级地向上寻找所有父对象的变量。

JavaScript-作用域、块级作用域、上下文、执行上下文、作用域链

一、函数 1、函数定义

函数是一段可以反复调用的代码块。函数可以接收输入的参数,不同的参数会返回不同的值

2、函数的声明方式

主要讲两种:

2.1 用function命令声明函数
function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数,函数体放在大括号里面

function print(s) {
  console.log(s);
}

2.2 用函数表达式声明函数
把匿名函数赋值给变量

var print = function(s) {
  console.log(s);
};
3、函数参数

3.1参数定义
参数:从外部传入函数,支撑函数运行的外部数据

3.2参数的传递规则
可以多传、少传参数,被省略的参数就是undefined。传递参数是按照顺序来适配的。

function printPersonInfo(name, age, sex){
    console.log(name)
    console.log(age)
    console.log(sex)
}
 printPersonInfo(sjz,male)//实际上name为sjz ,age为male,sex为undefined

3.3 arguments 对象
1)用途:arguments 对象可以在函数体内部读取所有参数
2)使用规则:arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
3)arugments对象长这样(下图是我传递了三个值)


3)通过arguments对象的length属性,可以判断函数调用时到底带几个参数
4) 举个例子

function printPersonInfo(name, age, sex){
    console.log(name)
    console.log(age)
    console.log(sex)
    console.log(arguments)
console.log(arguments[0])
console.log(arguments.length)
console.log(arguments[1] === age)
}
4、返回值

4.1 用return实现返回函数的操作后的数值,不写return语句,函数默认返回undefined
4.2 JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。
4.3返回值的应用
函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。

function fib(num) {
  if (num === 0) return 0;
  if (num === 1) return 1;
  return fib(num - 2) + fib(num - 1);
}

fib(6) // 8
5、函数声明前置

和变量的声明会前置一样,函数声明同样会前置的。分成两种情况:
5.1用function声明的函数
使用function声明的函数整个函数都会提升到代码头部。
所以你在声明函数前,调用了函数,都不会报错的,如下图

sum(3,4)
function sum(a,b){
return a+b; }
//7

5.2用函数表达式声明函数
不会把整个函数提升,只会把定义的变量提升到头部。

相当于如下代码。对于sum2就是一个未赋值的变量,为undefined,是不能作为函数执行的,所以报错了

var sum2;
sum2(3,4);
var sum2 =function (a,b){
return a+b ;} 
6、立刻执行的函数表达式

对于编辑器来说function (a,b){return a+b ;} 是一个函数声明语句,而不是一个函数类型的值,所以function (a,b){return a+b ;}加上()是会报错的

正确的写法是(function (a,b){return a+b ;})(), ()内部的东西是一个值,加上()代表立刻执行,整个语句相当于一个函数类型的值需要立刻执行

7、命名冲突

当在同一个作用域内定义了名字相同的变量和方法的话,会根据前置顺序产生覆盖

   var fn = 3;
    function fn(){}

    console.log(fn); // 3

相当于

var fn
function fn(){}  //覆盖上面的

fn = 3  //重新赋值
console.log(fn)
function fn(fn){
  console.log(fn);

  var fn = 3;
  console.log(fn);
}


fn(10) //10 3

相当于

function fn(){
var fn =arguments[0];
  console.log(fn);

  var fn = 3;
  console.log(fn);
}


fn(10) //10 3
二、函数作用域 1、定义

作用域(scope)指的是变量存在的范围

2、分类:

在 ES5 的规范中,Javascript 只有两种作用域:
一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;
另一种是函数作用域,变量只在函数内部存在。

3、全局变量和局部变量

函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。
在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)
javaScript 语言特有"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

4、作用域规则

{}不产生一个作用域,定义函数才会产生一个函数作用域

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

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

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
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
三、闭包 1、定义:

函数连同它作用域链上的要找的这个变量,共同构成闭包

2、特点

闭包最大的特点,就是它可以“记住”诞生的环境,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

3、用处

闭包的最大用处有两个

可以读取函数内部的变量

暂存数据(让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在)

4、举个栗子

如果没有这个闭包,函数执行后,里面speed变量就会被清理掉。但我们声明了fn这个函数,并把它返回出来赋值给新的变量speedup。因为speedup是全局变量,是一直存在的,故这个fn函数就一直存在,speed变量也不会被清理

function car(){
  var speed = 0
  function fn(){
    speed++
    console.log(speed)
  }
  return fn//重要,如果不return出来,相当于闭包的作用就没有了
}

var speedUp = car()
speedUp()   //1
speedUp()   //2
5、闭包经典案例

闭包的经典案例是定义一个变量,一个函数,一个return 函数。如上图
看一下这个案例如何改造

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

原理解析:for循环每次执行,都把function(){ return i} 这个函数赋值给fnArr[i],但这个函数不执行。因为fnArr[3] =function(){ return i};故当我们调用fnArr[3]() 时,相当于function(){ return i};这个函数立刻执行,这时for循环已经完成,i已经变成了10。故输出10

如果要输出3,需要如下改造

var fnArr = []
for (var i = 0; i < 10; i ++) {
  (function(i){
    fnArr[i] =  function(){
      return i
    } 
  })(i)
}
console.log( fnArr[3]() ) // 3
var fnArr = []
for (var i = 0; i < 10; i ++) {
  fnArr[i] =  (function(j){
    return function(){
      return j
    } 
  })(i)
}
console.log( fnArr[3]() ) // 3
四、作用域链

1、执行上下文
2、活动对象
Ao有两种来源,1、来自var定义的变量,2、传递的参数
3、scope属性
执行函数需要值得时候,就从活动对象AO里面找,找不到就从scope里面去找
4、例子1

var x = 10
bar()
function foo(){
console.log(x)
}
function bar(){
var x=30
foo()
}

1)全局上下文:
globalcontext ={
AO:{
x:10
foo:function
bar:function
},
scope:null
}
2)//声明foo函数得过程中,foo新增scope属性并指向了globalContext的Ao
foo.[[scope]] =globalContext.Ao
//声明bar函数得过程中,bar新增scope属性并指向了globalContext的Ao
bar.[[scope]] =globalContext.Ao
在执行上下文的声明的函数,这个函数的[[scope]] 就等于globalContext(执行上下文)的Ao

3)当调用bar的时候,进入了bar的执行上下文
barcontext ={
AO:{
x:30
},
scope:bar.[[scope]] // globalContext.Ao
}创建bar的过程中,bar新增scope属性并指向了globalContext的Ao

4)当调用foo的时候,进入了foo的执行上下文
foocontext ={
AO:{},
scope:foo.[[scope]] // globalContext.Ao
}

5、例2

var x = 10
function bar(){
var x=30
function foo(){
console.log(x)
}
foo()
}
bar()

1)全局上下文:
globalcontext ={
AO:{
x:10
bar:function
},
scope:null
}

2)//声明bar函数得过程中,bar新增scope属性并指向了globalContext的Ao
bar.[[scope]] =globalContext.Ao

3)当调用bar的时候,进入了bar的执行上下文
barcontext ={
AO:{
x:30
foo:function
},
scope:bar.[[scope]] // globalContext.Ao
}创建foo的过程中,foo新增scope属性并指向了barcontext的Ao
foo.[[scope]] =balContext.Ao

4)当调用foo的时候,进入了foo的执行上下文
foocontext ={
AO:{},
scope:foo.[[scope]] // balContext.Ao
}
所以console.log(x)是30

6、例子3
封装一个 Car 对象
car对象封装4个接口,我们只能通过提供接口来操作数据,不能直接操作数据。
原理:定义一个car对象,设置其等于一个立刻执行的函数表达式 中return出来的内容。
return出来的对象,有四个属性(setSpeed,get,speedUp,speedDown),四个属性分别对应了四个函数(setSpeed,get,speedUp,speedDown)。这四个函数就用于操作speed的值。这导致car得不到释放,return的变量也无法释放,对应的所有函数都没有办法释放,就生成了一个闭包

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

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

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

1)原理:我们设置了延时为0的定时器,每次for循环一次的时候,就把函数的代码添加到异步队列里面一次。当for循环5次循环完之后,开始执行5次的函数,函数执行时去找i的值,这时候的i的值已经变成5,所以就连续输出5个5

2)改造

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

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

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

8、例5

function makeCounter() {
  var count = 0

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

var counter = makeCounter()//**相当于把返回的function() {return count++}这个函数赋值counter**
var counter2 = makeCounter();//**然后把第二次返回的function() {return count++}这个函数赋值counter2**

console.log( counter() ) // 0 //**counter() 每执行一次,就会返回一个数值加1的counter值**
console.log( counter() ) // 1

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

原理:因为形成了一个闭包 , counter和counter2 返回的函数存的不是同一个地址,所以对于counter和counter2对应的活动对象是不一样的

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

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

解析:sum(1)之后能跟着一个(),表示sum(1)是一个还没有执行的函数,等于function sum(){
return function(){}}。
sum(1)后面接了一个(2)表示返回的函数要接收一个参数,本身也要接受一个参数。function sum(a){
return function(b){}
}
最后根据这个函数的功能,返回a+b的值

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

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

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

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"))

sort后面必须要接受一个函数,所以需要返回一个参数。

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"))

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108704.html

相关文章

  • [JS]《你不知道的Javascript·上》——词法作用域和闭包

    摘要:吐槽一下,闭包这个词的翻译真是有很大的误解性啊要说闭包,要先说下词法作用域。闭包两个作用通过闭包,在外部环境访问内部环境的变量。闭包使得函数可以继续访问定义时的词法作用域。 闭包是真的让人头晕啊,看了很久还是觉得很模糊。只能把目前自己的一些理解先写下来,这其中必定包含着一些错误,待日后有更深刻的理解时再作更改。 吐槽一下,闭包这个词的翻译真是有很大的误解性啊…… 要说闭包,要先说下词法...

    guqiu 评论0 收藏0
  • JavaScript面向对象~ 作用域和闭包

    摘要:大名鼎鼎的作用域和闭包,面试经常会问到。声明理解闭包,先理解函数的执行过程。闭包的基本结构因为闭包不允许外界直接访问,所以只能间接访问函数内部的数据,获得函数内部数据的使用权。 大名鼎鼎的作用域和闭包,面试经常会问到。闭包(closure)是Javascript语言的一个难点,也是它的特色。 声明 理解闭包,先理解函数的执行过程。 代码在执行的过程中会有一个预解析的过程,也就是在代码的...

    WilsonLiu95 评论0 收藏0
  • 你应该要知道的作用域和闭包

    摘要:写在前面对于一个前端开发者,应该没有不知道作用域的。欺骗词法作用域有两个机制可以欺骗词法作用域和。关于你不知道的的第一部分作用域和闭包已经结束了,但是,更新不会就此止住未完待续 这是《你不知道的JavaScript》的第一部分。 本系列持续更新中,Github 地址请查阅这里。 写在前面 对于一个前端开发者,应该没有不知道作用域的。它是一个既简单有复杂的概念,简单到每行代码都有它的影子...

    JouyPub 评论0 收藏0
  • 什么是闭包,变量的作用域和自执行函数

    摘要:作用域和闭包以及自执行函数作用域作用域分为种全局作用域全局作用域就是在的任何位置都能访问过函数作用域只能在函数里面调用的称之为函数作用域闭包嵌套在函数里面的函数及周边的变量叫闭包闭包存在的问题是周边变量不会被释放,常驻内存中闭包的缺点消耗内 作用域和闭包以及自执行函数 作用域 作用域分为2种 1、全局作用域 全局作用域就是在js的任何位置都能访问过 2、函数作用域 只能在函数里面调用的...

    daydream 评论0 收藏0
  • 图解JS闭包形成的原因

    摘要:闭包的出现正好结合了全局变量和局部变量的优点。这就是闭包的一个使用场景保存现场。 前言 什么是闭包,其实闭包是可以重用一个对象,又保护对象不被篡改的一种机制。什么是重用一个对象又保护其不被篡改呢?请看下面的详解。 作用域和作用域链 注意理解作用域和作用域链对理解闭包有非常大的帮助,所以我们先说一下作用域和作用域链 什么是作用域作用域表示的是一个变量的可用范围、其实它是一个保存变量的对象...

    wind3110991 评论0 收藏0
  • 什么是闭包?变量作用域和闭包

    摘要:在上面的代码中,函数实际上就是函数的闭包函数,我们让其执行三次,结果分别为。这是因为,函数中的局部变量一直被保存在内存中。所以闭包有个缺点,就是内存占用较大。自执行函数上面这段函数也是闭包的一种。我们利用闭包来做一个小例子。 变量作用域和闭包 变量作用域 当我们写 js 文档的时候经常会设置变量,变量的类型有两种: 全局变量 局部变量 这两种类型的变量有者不同的作用范围,全局变量的...

    U2FsdGVkX1x 评论0 收藏0

发表评论

0条评论

Lin_YT

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<