【JS高级程序设计】第七章 函数表达式

【JS高级程序设计】第七章 函数表达式

函数表达式是 JavaScript 中的一个既强大又容易令人困惑的特性。第 5 章曾介绍过,定义函数的 方式有两种:一种是函数声明,另一种就是函数表达式。函数声明的语法是这样的。

function functionName(arg0, arg1, arg2) { 
  //函数体 
}

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

//只在 Firefox、Safari、Chrome 和 Opera 有效 alert(functionName.name); //"functionName"

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

sayHi(); function sayHi(){ alert("Hi!"); }

这个例子不会抛出错误,因为在代码执行之前会先读取函数声明。

第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一 种形式。

var functionName = function(arg0, arg1, arg2){ //函数体 };

这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。 这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。 (匿名函数有时候也叫拉姆达函数。)匿名函数的 name 属性是空字符串。

7.1 递归

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

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

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

var anotherFactorial = factorial; 
factorial = null;
alert(anotherFactorial(4)); //出错!

以上代码先把 factorial()函数保存在变量 anotherFactorial 中,然后将 factorial 变量设 置为 null,结果指向原始函数的引用只剩下一个。但在接下来调用 anotherFactorial()时,由于必 须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误。在这种情况下,使用 arguments.callee 可以解决这个问题。

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

但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可 以使用命名函数表达式来达成相同的结果。

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

以上代码创建了一个名为 f()的命名函数表达式,然后将它赋值给变量 factorial。即便把函数 赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和 非严格模式下都行得通。

7.2 闭包

有不少开发人员总是搞不清匿名函数和闭包这两个概念,因此经常混用。闭包是指有权访问另一个 函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,仍以前面的 createComparisonFunction()函数为例,注意加粗的代码。

function createComparisonFunction(propertyName) {

return function(object1, object2){

var value1 = object1[propertyName]; 
var value2 = object2[propertyName];

if (value1 < value2){ return -1; } else if (value1 > value2){

return 1; } else {

return 0; }

};

}

在这个例子中,突出的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部 函数中的变量 propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可 以访问变量 propertyName 。 之所以还能够访问这个变量, 是因为内部函数的作用域链中包含 createComparisonFunction()的作用域。要彻底搞清楚其中的细节,必须从理解函数被调用的时候 都会发生什么入手。

第 4 章介绍了作用域链的概念。而有关如何创建作用域链以及作用域链有什么作用的细节,对彻底 理解闭包至关重要。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。 然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域 链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。 在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。来看下面的例子。

function compare(value1, value2){ 
  if (value1 < value2){
    return -1; 
  } else if (value1 > value2){ 
    return 1; 
  } else {
    return 0; 
  }

} var result = compare(5, 10);

以上代码先定义了 compare()函数,然后又在全局作用域中调用了它。当调用 compare()时,会 创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare )在 compare() 执行环境的作用域链中则处于第二位。

作用域链本质上是一个指向变量对象的指针列表,它只 引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。 但是,闭包的情况又有所不同。

7.2.1 闭包与变量

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

function createFunctions(){
  var arr = new Array()
  for(var i = 0 ; i < 10 ; i++){
    arr[i] = function(){
      return i
    }
  }
  return arr
}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数 返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中 都 保 存 着 createFunctions() 函 数 的 活 动 对 象 , 所 以 它 们 引 用 的 都 是 同 一 个 变 量 i 。 当 createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量 对象,所以在每个函数内部 i 的值都是 10。

function createFunctions(){
  var arr = new Array()
  for(var i = 0 ; i<10; i++){
    arr[i] =  function(a){
     return function(){
       return a
     }
    }(i)
  }
  return arr
}

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

7.2.2 关于this对象

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

var name = 'window'
var obj = {
  name: 'im obj',
  getName:function(){
    return function(){
      return this.name
    }
  }
}
obj.getName()() // window

以上代码先创建了一个全局变量 name,又创建了一个包含 name 属性的对象。这个对象还包含一 个方法——getNameFunc(),它返回一个匿名函数,而匿名函数又返回 this.name。由于 getNameFunc() 返回一个函数,因此调用 object.getNameFunc()()就会立即调用它返回的函数,结果就是返回一个 字符串。然而,这个例子返回的字符串是”The Window”,即全局 name 变量的值。为什么匿名函数没 有取得其包含作用域(或外部作用域)的 this 对象呢?

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

var name = 'window'
var obj = {
  name: 'im obj',
  getName:function(){
    var that = this
    return function(){
      return that.name
    }
  }
}
obj.getName()() // im obj

使用闭包的特性把当前this值赋给that,通过闭包的形式可以访问到当前that的对象

代码中突出的行展示了这个例子与前一个例子之间的不同之处。在定义匿名函数之前,我们把 this 对象赋值给了一个名叫 that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们 在包含函数中特意声名的一个变量。 即使在函数返回之后,that 也仍然引用着 object,所以调用 object.getNameFunc()()就返回了”My Object”。

7.3 模仿块级作用域

如前所述,JavaScript 没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含 函数中而非语句中创建的,来看下面的例子。

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

这个函数中定义了一个 for 循环,而变量 i 的初始值被设置为 0。在 Java、C++等语言中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScrip 中,变量 i 是定义在 ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即 使像下面这样错误地重新声明同一个变量,也不会改变它的值。

7.4 私有变量

严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有 变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

【JS高级程序设计】第五章 引用类型

【JS高级程序设计】第五章 引用类型

引用类型的值(对象)是引用类型的一个实例。在 ECMAScript 中,引用类型是一种数据结构, 用于将数据和功能组织在一起。它也常被称为类,但这种称呼并不妥当。尽管 ECMAScript 从技术上讲是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构。引 用类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。

阅读更多
【JS高级程序设计】第六章 面向对象的程序设计

【JS高级程序设计】第六章 面向对象的程序设计

面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可 以创建任意多个具有相同属性和方法的对象。前面提到过,ECMAScript 中没有类的概念,因 此它的对象也与基于类的语言中的对象有所不同。

阅读更多
【JS高级程序设计】第四章 变量、作用域和内存问题
【JS高级程序设计】第三章 基本概念

【JS高级程序设计】第三章 基本概念

3.1语法

  • 区分大小写

  • 不能使用关键字/保留字

  • 统一的命名规则

    第一个字符必须是一个字母、下划线(_)或一个美元符号($); 其他字符可以是字母、下划线、美元符号或数字。 标识符中的字母也可以包含扩展的 ASCII 或 Unicode 字母字符(如 À 和 Æ),但我们不推荐这样做。

    ECMAScript 标识符采用驼峰大小写格式:

    firstSecond myCar doSomethingImportant

    虽然没有谁强制要求必须采用这种格式,但为了与 ECMAScript 内置的函数和对象命名格式保持一 致,可以将其当作一种最佳实践。

阅读更多
【JS高级程序设计】 第二章 使用JavaScript

【JS高级程序设计】 第二章 使用JavaScript

2.1 “script”元素

向 HTML 页面中插入 JavaScript 的主要方法,就是使用</script>元素。这个元素由 Netscape 创造 并在 Netscape Navigator 2 中首先实现。后来,这个元素被加入到正式的 HTML 规范中。HTML 4.01 为 </script>定义了下列 6 个属性:

阅读更多

CentOS 防火墙 启动/停止/重启/永久开启端口

# >>> 开启端口
firewall-cmd --zone=public --add-port=8000/tcp --permanent
#  命令含义:
--zone #作用域
--add-port=80/tcp #添加端口,格式为:端口/通讯协议
--permanent #永久生效,没有此参数重启后失效
# >>> 重启防火墙
firewall-cmd --reload                          ##重启防火墙
firewall-cmd --state                           ##查看防火墙状态,是否是running
firewall-cmd --reload                          ##重新载入配置,比如添加规则之后,需要执行此命令
firewall-cmd --get-zones                       ##列出支持的zone
firewall-cmd --get-services                    ##列出支持的服务,在列表中的服务是放行的
firewall-cmd --query-service ftp               ##查看ftp服务是否支持,返回yes或者no
firewall-cmd --add-service=ftp                 ##临时开放ftp服务
firewall-cmd --add-service=ftp --permanent     ##永久开放ftp服务
firewall-cmd --remove-service=ftp --permanent  ##永久移除ftp服务
firewall-cmd --add-port=80/tcp --permanent     ##永久添加80端口 
iptables -L -n                                 ##查看规则,这个命令是和iptables的相同的
man firewall-cmd                               ##查看帮助
阅读更多
JavaScript this
JavaScript 函数

JavaScript 函数

函数概念

函数是一块 JavaScript代码,被定义一次,但可执行和调用多次。JS中的函数也是对象,所以JS函数可以像其它对象那样操作和传递,所以我们也常叫JS中的函数为函数对象。

阅读更多
JavaScript 数组

JavaScript 数组

创建数组

数组是特殊的Object,包含很多方法,创建数组的方法有很多:

//数组的创建
var arr  = new Array(100) //创建一个长度为100的数组,所有的值都为undefined
var arr1 = new Array('1','2',true) //创建一个包含数值和固定下标的字符串
var arr2 = [] //快捷创建一个数组 

数组的方法

获取所有的数组方法: Array.prototype 所有在原型链上的方法

阅读更多