函数表达式

本文主要介绍,函数表达式特征、使用函数实现递归、使用闭包定义私有变量。

函数表达式特征

函数表达式是JavaScript中的一个即强大又容易令人困惑的特性。定义函数的方式有两种:函数声明、函数表达式

  • 函数声明
// 函数声明
function fn() {
  console.log('Hello world')
}

首先是 function 关键字,然后是函数的名字,这就是指定函数名的方式。这样会给函数指定一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于 跟在 function 后面的标识符

关于函数声明,他有一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以吧函数放在调用它的语句后面

fn() // Hello
function fn() {
  console.log('fn')
}
  • 函数表达式
// 函数表达式
var fn = function() {
 // todo
}

这种方式类似于变量的复制,这种情况下创建的函数叫做匿名函数( anonymous function),因为function关键字后面没有标识符。(匿名函数也叫作拉姆达函数。)匿名函数的name属性时空字符串

sayHi()
var sayHi = function() { console.log('hh') }

理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。var声明的变量也会提升,但初始值是undefined

递归

递归函数时在一个函数通过名字调用自身的情况下构成的,如下所示。

function factorial(num) {
  if(num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

这是一个经典的递归阶乘函数。虽然整函数表面上看来没什么问题,但下面的代码可能导致他出错

var anotherFactorial = factorial
factorial = null
console.log(antherFactorial(10)) // throw error

上面的代码,将factorial重新赋值为null,调用 anotherFactorial后,通过递归会调用 factorial,而 factorial 已经不是函数,所以就会导致错误。这种清情况下可以使用 arguments.callee 来解决这个问题

function factorial(num) {
  if (num <= 1) { // 递归出口
    return 1
  } else {
    retun num * arguments.callee(num -1)
  }
}

使用arguments.callee 来代替函数名,可以确保物流怎样调用函数都不会出现问题。所以在递归的时候,使用 arguments.callee 总比使用函数名 保险

var factorial = (function f(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * f(num -1)
  }
});

上面这个实例,也是一种很独特的方式,将函数赋值给另一个变量,函数的名字f 任然有效,所以递归调用照样能正确完成。这种方式在严格模式和 非严格模式下都行得通

闭包

闭包是一个比较抽象的概率。闭包是指有权访问另一个作用域中的变量和函数。创建比表的常见方式,就是在一个函数内部创建另一个函数

function createComparisonFunction( propertyName ) {
  return function(obj1, obj2) {
    const val1 = object1[propertyName] // 访问外部变量 propertyName
    const val2 = object2[propertyName] // 访问外部变量 propertyName

    if (val1 < val2) {
      return -1
    } else if (val1 > val2) {
      return 1
    } else {
      return 0
    }
  }
}

上面实例中的内部函数被返回了,而且是在其他地方被调用,但它仍然可以访问变量 propertyName。之所以能够访问这个变量,是因为内部函数的作用域中包含 外部函数的 作用域。

有关 如何常见作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。当某个函数别调用时,会创建一个执行环境(execution context) 及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象( activeation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位, ....直到作为作用域链终点的全局执行环境

在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量。

function compare(val1, val2) {
  if (val1 < val2) {
    return -1
  } else if (val1 > val2) {
    return 1
  } else {
    retun 0
  }
}

let res = compare(1, 2)

当调用 compare() 是,会创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare) 在 compare() 执行环境的作用域链中处于第二位。
后台的每个执行环境都有一个表示变量的对象——变量对象。全局的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
在创建compare() 函数时,会创建一个预先包含全局变量对象的作用域链,而这个作用域链被保存在内部的 [[ Scope ]] 属性中。当调用compare() 函数时,会为函数创建一个执行环境,然后通过复制函数的[[ Scope ]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端
对于这个例子中 compare() 函数的执行环境而言,其作用域链中包含两个变量对象;本地活动对象和全局变量对象。显然,作用域链本质上是一个执行变量对象的执政列表,他只引用但不实际包含变量对象

在匿名函数从 createComparisonFunction() 中被返回后,它的作用域链被初始化为包含 createComparisonFunction() 函数的活动对象和 全局对象。这样,匿名函数就可以访问在 createComprisonFunction() 中定义的所有变量。跟为重要的是,createComparisonFunction() 函数在执行完毕之后,其活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;知道匿名函数被销毁后, createComparisonFunction() 的活动对象才会被销毁

// 创建函数
let compareNames = createComparisonFunction('name')

// 调用函数
let res = compareName(1, 3)

// 解除对匿名函数的引用(以便释放内存)
compareName = null

首先,将创建的比较函数保存在变量 compareNames中。最后将 compareNames 设置为 等于 null 解除该函数的引用,就等于通知垃圾回收例程将其清除

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特使的变量。

function createFunction() {
  let res = new Array()
  
  for ( var i = 0; i < 10; i++){
    res[i] = function() {
      return i
    }
  }

  return res
}

这个函数返回一个数组,表面上看。似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10,。因为每个函数作用域中都保存在 createFunction() 函数的活动对象,所有它们引用的都是同一个变量 i。当 createFunction() 函数返回后,变量 i 的值是 10,此时每个函数都引用这变量 i 的 同一个变量对象,所有在每个函数内部 i 的值都是10。但是,我们可以创建另一个匿名函数强制让闭包的行为符合预期

function createFunction() {
  let res = new Array()
  
  for( var i = 0; i < 10; i++) {
    res[i] = function(num) { // 自调用函数
      // 返回一个匿名函数
      return function() {
        return num
      }
    }(i); // 传入参数
  }

  return res
}

上面重写了前面createFunction()函数后,每个函数就会返回各自不同的索引值。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num, 也就是最终的函数要返回的值。
由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num的包。这样一来,res、 数组中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。

关于 this 对象

在闭包中 使用 this 对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象z通常指向window。但有时由于编写闭包的方式不同,这一点可能不会那么明显.

var name= 'The Window'

var obj = {
  name: 'My Object',
  getNameFunc() {
    return function() {
      return this.name
    }
  }
}

console.log(obj.getNameFunc()()) // The Window

前面提到过,每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这个变量时,只会搜索到其活动对象(arguments 和 其他命名参数)为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域的this对象保持在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

var name = 'The Window'

const obj = {
 name: 'My Object',
 getName() {
   const that = this
   return function() {
     return that.name
   }
 }
}
console.log(obj.getName()()) // My Object

我们把this 对象赋值给了一个名叫 that的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为他是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that 也任然引用 这 object

在几种特殊的情况下,this的值可能会以外地改变。

var name = 'The Window'

const obj = {
  name: 'My Object',
  getName: function() {
    console.log(this.name)
  }
}

obj.getName() // 'My Object'
;(obj.getName)() // 'My Object'
;(obj.getName = obj.getName)() // 'The Window' 在非严格模式下

第一个是普通的调用返回的是预期的结果
第二个在调用这个方法前给它加上了括号。虽然加上括号之后,好像只是在引用一个函数,但是this的值得到了维持,因此 obj.getName 和 (obj.getName) 的定义是相同的。
第三个,首先执行了一个赋值语句,然后在调用赋值后的结果。因为这个赋值表达式的值是函数本身(一个脱离上下文的匿名函数), 所有this的值不能得到维持,结果就返回了 The window
当然,后两种方式不大可能会使用。不过,这个例子有助于说明即使是语法的细微变化,都有可能改变this的值。

内存泄漏

由于IE9之前的版本对JScript对象和 COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法销毁。

function assignHandler() {
  var element = document.getElementById('someElement')

  element.onclick = function() {
    console.log(element.id)
  }
}

由于匿名函数保存了一个队 assignHandler() 的活动对象的引用,因此就会导致无法减少element的引用次数。只要匿名函数存在,element的引用次数至少也是1,因此它所占用的内存就永远不会被回收。

function assignHandler() {
  var element = document.getElementById('someElement')
  var id = element.id
  
  element/onclick = function() {  
    console.log(id)
  }
  element = null
}

上面的代码经过改写,将我们需要的属性保存在变量 id 中。但仅仅做到这一步,韩式不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中也包含着element。即使闭包不直接使用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要吧element 变量设置为 null。这样就能够解除对DOM对象的引用,顺利的减少其引用次数,确保正常回收其占用的内存。

模仿块级作用域

JavaScript 中没有块级作用域的概念。这意味着在快语句中的变量,实际上是在包含函数中而非语句中创建的

function outputNumbers(count) {
  for (var i = 0; i < count; i++){
    console.log(i)
  }
  console.log(i) 
}

在JavaScript中,变量i是定义在outputNumbers() 的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即使项下面这样错误地重新声明同一个变量,也不会改变它的值。

function outputNumbers(count) {
  for (var i = 0; i < count; i++){
    // console.log(i)
  }
  var i 
  console.log(i)
}
outputNumbers(10)

var 关键字可以多次申明同一个变量;遇到这种情况,他只会对后续的申明视而不见(不过,它会执行后续申明中的变量初始化。也有变量提升的原因)。匿名函数可以用来模仿块级作用域并避免这个问题。

// 自调用函数,形成块级作用域
(function() {
  // todo
})()

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:

function outputNumbers(count) {
  (function() {
    for(var i = 0; i  < count; i++) {
      // todo
    }
  })()

  console.log(i) // throw error
}

我们在for循环外部插入一个私有作用域,在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。此外,在私有作用域中能够访问变量 count, 是因为这个匿名函数是一个闭包,他能够访问闭包作用域中的所有变量。

这种技术经常在全局作用域中被用在函数外部。从而限制想全局作用域中添加过多的变量和函数。同时没有执行匿名函数的引用。只有函数执行完毕,就可以立即销毁其作用域链了。

私有变量

严格来讲,在JavaScript中没有私有成员的概念;所有对象属性都是共有的。不过,倒是有一个私有变量的改了。任何函数中定义的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数全局变量函数内部定义的其他函数
我们把有权访问私有变量和私有函数的公有方法称为特权方法( privileged method )。有两种子啊对象上创建特权方法的方式。

第一中是在构造函数中定义特权方法

function MyObject() {
  // 私有变量 和 私有函数
  var privateVariable = 10
  
  function privateFunction() {
    return false
  }

  // 特权方法 
  this.publicMethod = function() {
    provateVariable++
    return privateFunction()
  }
}

这个模式在构造函数内部定义了所有私有变量和函数。然后,有创建了能够访问这些私有成员的特权方法。因为特权方法作为闭包有权访问构造函数的活动对象。如上,创建实例过后除了调用 publicMethod 没有其它途径去访问 privateVariable 和 privateFunction

除此之外,还可以使用私有和特权成员,可以隐藏那些不应该被直接修改的数据

function Person(name) {
  this.getName = function() {
    return name
  }
  this.setName = function(val) {
    name = val
  }
} 

const person = new Person('了凡')
person.setName('纤风')
console.log(person.getName()) // 纤风

以上实例,定义了量特权方法:getName() 和 setName()。这两个方法都可以在实例上使用,除此之外没有任何办法访问name。但构造函数存在的缺点是,针对每个实例都会创建一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下。

;(function() {
  // 私有变量和私有函数
  var privateVariable = 10
  function privateFunction() {
    return false
  }

  // 构造函数
  MyObject = function() { // 隐式全局
  }

  // 公共 / 特权方法
  MyObject.prototype.publicMethod = function() {
    privateVariable++
    return privateFunction()
  }
})()

这个模式创建了一个私有作用域,使用原型继承的方式。构造函数使用函数表达式的方式,并且未使用var申明(全局变量),因此能够在私有作用域外访问。需要注意的是严格模式下会导致错误。
这个模式与构造函数中特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都是以同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

;(function() {
  let name = ''
  Person = function(value) {
    name = value
  }
  Person.prototype.getName = function() {
    return name
  }
  Person.prototype.setName = function(val) {
    name = val
  }
})()
const person1 = new Person('云裳')
console.log(person1.getName()) // 云裳
person1.setName('皓霜')
console.log(person1.getName()) // 皓霜

const person2 = new Person('纤风')
console.log(person2.getName()) // 纤风
person2.setName('了凡')
console.log(person2.getName()) // 了凡

这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说一个实例上调用了setName() 就会影响到所有实例。这种方式创建静态私有变量就会因为原型而增进代码复用,但每个实例都没有自己的私有变量。

多查找作用域链种的一个层次,就会在一定程度上影响查找速度。而这正式使用闭包和私有变量的一个明显不足之处。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式( module pattern ) 则视为单例创建私有变量和特权方法。所谓单例( singleton ),指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。

var singleton = {
  name: value,
  method: function() {
    // todo
  }
}

模块开发通常为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

var singleton =  function() {
  // 私有变量 和 私有函数
  var privateVariable = 10
  function privateFunction() {
    return false
  }
  // 特权 / 公有方法和属性
  return {
    publicProperty: true,
    publicMethod: function() {
      privateVariable++
      return privateFunction()
    }
  }
}()

返回的对象字面量种包含可以公开的属性和方法。由于这个对象实在匿名函数内部定义的,因此它的共有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时很有作用。

var application = function() {
  // 私有变量和函数
  var components = new Array()
  // 初始化
  components.push(new BaseComponent())

  // 公共
  return {
    getComponentCount() {
      return components.length
    },
    registerComponent(component) {
      if (typeof component == 'object') {
        components.push(component)
      }
    }
  }
}()

这给例子创建了一个用于管理组件的 application 对象。首先申明了一个 私有的 components 数组,随后对他进行了初始化(这里不需要在意baseComponent)。而返回对象的getComponentCount() 和 registerComponent() 方法,都是有权访问数组的特权方法。

简言之,如果必须创建一个对象并以某些数据对齐进行初始化,同时还要公开一些能够反问这些私有数据的方法,那么就可能是以模式。以这种模式创建的每个实例都是Object的实例,因为最终要通过一个对象字面量来表示它。

增强的模块模式

有人进一步改进了模块模式,即在返回对象之前加入对齐增强的代码。这种增强的模块适合那些单例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的目的。

var singleton = function() {
  var privateVariable = 10
  function provateFunction() {
    return false
  }

  // 创建对象
  var obj = new CustomType()
  
  // 添加特权/公共属性和方法
  obj.publicMethod = function() {
    privateVariable++
    return privateFunction()
  }
  // 返回这个对象
  return obj
}()

如果前面演示模块模式的例子中 application 对象必须是 BaseComponent 的实,那么就可以使用如下代码。

var application = function() {
  // 私有变量和函数
  var components = new Array()
  // 初始化
  components.push(new BaseComponent())

  // 创建 application 的一个局部符本
  var app = new BaseComponent()

  // 公共接口
  app.getComponentCount = function() {
    return components.length
  }
  app.registerComponent = function(component) {
    if (typeof component == 'object') {
      components.push(compoent)
    }
  }

  // 返回这个副本
  return app
}()

小结:

在JavaScript中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。

  • 递归函数一个尽量使用 arguments.callee来进行调用,而不是函数名——可能会发生变化
  • 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
  • 通常,函数的作用域及其所有变量都会在函数执行结束后销毁。
  • 当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。使用闭包可以在JavaScript中模仿块级作用域。
  • JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法 ,而通过公有方法可以访问在包含作用域中的变量。
  • 有权访问私有变量的公又方法叫特权方法。
  • 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  •   函数表达式是 JavaScript 中的一个既强大有容易令人困惑的特性。定义函数的的方式有两种: 函数声明; ...
    霜天晓阅读 804评论 0 1
  • 定义函数的方式有两种:函数声明和函数表达式。 函数声明的一个重要特征就是函数声明提升,意思是在执行代码前会先读取函...
    oWSQo阅读 655评论 0 0
  • 定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。函数声明的语法: 关于函数声明的一个重要特征就是函数声...
    LemonnYan阅读 77评论 0 0
  • 第七章:函数表达式 本章内容: 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 定义函数的方式有两种,一...
    穿牛仔裤的蚊子阅读 363评论 0 1
  • 本章内容 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 定义函数的方式有两种:一种是函数声明,另一种就...
    闷油瓶小张阅读 347评论 0 0