资讯专栏INFORMATION COLUMN

《JavaScript高级程序设计》(第3版)读书笔记 第7章 函数表达式

邹立鹏 / 470人阅读

摘要:定义函数表达式的方式有两种函数声明。不过,这并不是匿名函数唯一的用途。可以使用命名函数表达式来达成相同的结果闭包匿名函数和闭包是两个概念,容易混淆。匿名函数的执行环境具有全局性,因此其对象通常指向通过改变函数的执行环境的情况除外。

定义函数表达式的方式有两种:

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

函数表达式。

// 函数声明
function functionName(params) {
  ...
}
// 函数表达式有几种不同的方式,下面是最常见的一种
var functionName = function(params) {
  ...
}

上面这种形式看起来好像是常规的变量赋值语句。而右边这种形式创建的函数叫做 匿名函数 (anonymous function)(有时候也叫 拉姆达函数 lambda),因为function关键字后面没有标识符。

函数表达式与其他表达式一样,在使用前必须先赋值,否则会导致出错。

sayHi();         // 错误,函数还不存在
var sayHi = function () {
  console.log("Hi!");
};

表面上看,下面的代码没有问题,conditiontrue时,使用一个定义,否则使用另一个定义。实际上,在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理状态。但问题是浏览器尝试修正错误的做法并不一致。大多数浏览器会返回第二个声明,忽略condition的值;Firefox会在condition为true的时候返回第一个声明。因此这种做法很危险,不应该出现在你的代码中。

// 不要这样做!
if (condition) {
  function sayHi() {
    console.log("Hi!");
  }
} else {
  function sayHi() {
    console.log("Yo!");
  }
}

上述代码改为函数表达式就没有问题

// 可以这样做
var sayHi;

if (condition) {
  sayHi = function() {
    console.log("Hi!");
  }
} else {
  sayHi = function() {
    console.log("Yo!");
  }
}

能够创建函数再赋值给变量,也就能把函数作为其他函数的返回值。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;
    }
  }
}

在把函数当成值来使用的情况下,都可以使用匿名函数。不过,这并不是匿名函数唯一的用途。

递归

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

// 经典的递归阶乘函数
function factorial (num) {
  if (num <= -1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

虽然递归阶乘函数表面看没有什么问题,但下面的代码却可能导致它出错

// 把factorial() 函数保存在变量anotherFactorial中
var anotherFactorial = factorial;

// 将factorial设置为null
// 现在指向原始函数的引用只剩下anotherFactorial
factorial = null;

// 原始函数必须执行factorial()
// 但factorial不再是函数,所以导致出错
anotherFactorial(4);      // throw error!

可以使用 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);
  }
})
闭包

匿名函数闭包 是两个概念,容易混淆。 闭包 是指有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方式,就是在一个函数内部创建另一个函数,仍以前面的 createComparisonFunction() 函数为例

function createComparisonFunction (propertyName) {
  
  return function (object1, object2) {

    // 下面两行代码访问了外部函数中的变量propertyName
    // 即使这个内部函数被返回了,而且是在其他地方被调用了
    // 它仍然可以访问变量 propertyName
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

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

上述例子,即使内部函数被返回了,在其他地方调用,它仍然可以访问propertName。因为这个内部函数的作用域链中包含 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() 函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]属性中。

当调用 compare() 函数时,会为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链。

此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。

对于本例, compare() 函数的执行环境而言,其作用域链中包含两个变量对象:

本地活动对象

全局活动对象

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

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般而言,当函数执行完毕后,局部活动对象就会被销毁,内存中近保存全局作用域(全局执行环境的变量对象)。

但是闭包的情况有所不同

function createComparisonFunction (propertyName) {
  
  return function (object1, object2) {

    // 下面两行代码访问了外部函数中的变量propertyName
    // 即使这个内部函数被返回了,而且是在其他地方被调用了
    // 它仍然可以访问变量 propertyName
    // 即为 createComparisonFunction 的活动对象
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

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

// 创建比较函数
// 调用了 createComparisonFunction() 方法
// 创建了 createComparisonFunction 的活动对象
// 返回内部的匿名函数 保存在 compareNames
// createComparisonFunction 执行完毕
// 但它的活动对象仍被 内部匿名函数引用,所以活动对象仍然存在,不会销毁
var compareNames = createComparisonFunction("name");

// 此时result调用了 保存在 compareNames 的匿名函数
// 该匿名函数保持了对 createComparisonFunction 活动对象的引用
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });

// 即使 compareNames 执行完毕,createComparisonFunction 活动对象依然存在
// 需要手动解除对匿名函数的引用(以便释放内存)
compareNames = null;

首先,创建的比较函数被保存在变量compareNames中,而通过将其设置为null解除引用,就等于通知垃圾回收例程将其消除。随着匿名函数的作用域链被销毁,其他作用域链(除了全局作用域)也都可以安全地的销毁了。图7-2展示了调用compareNames()的过程中产生的作用域链之间的关系

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像V8等优化后的JavaScript引擎会尝试回收被闭包占用的内存,但请还是要谨慎使用。

闭包与变量

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

function createFunctions() {
  var result = new Array();

  for (var i=0; i < 10; i++) {
    // 赋值给数组元素的是匿名函数本身,不是具体的值
    // 所以在 createFunctions() 执行完毕后,调用数组内的函数,返回的是变量i的值
    // 而变量i在执行完毕后,等于 10
    result[i] = function() {
      // 返回指向变量 i 的指针
      return i;
    };
  }

  return result;
}

这个函数会返回一个 函数数组。表面上看result里的每一项函数都应该返回自己的索引值。但实际上每一个函数返回的都是10

因为每个函数的作用域中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10.

可以通过创建另一个匿名函数强制让闭包的行为符合预期

function createFunctions() {
  var result = new Array();

  for (var i=0; i < 10; i++) {
    // 此时返回的里层匿名函数调用了外层匿名函数的 num
    // 里层匿名函数创建并返回了一个访问 num 的闭包
    // 如此一来 result 数组中的每个函数都有自己的num变量副本
    result[i] = function(num) {
      // 返回创建的另一个匿名函数
      return function() {
        return num;
      };
    }(i);
  }

  return result;
}
关于this对象

在闭包中使用this对象也可能会导致一些问题。this对象是在运行时基于函数的执行环境绑定的:

在全局函数中,this等于window

当函数被作为某个对象的方法调用时,this等于那个对象。

匿名函数的执行环境具有全局性,因此其this对象通常指向window(通过call() apply()改变函数的执行环境的情况除外)。但有时候由于变成写闭包的方式不同,这一点可能不会那么明显

var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function() {
    return function() {
      return this.name;
    };
  }
};

// 在非严格模式下
object.getNameFunc()();                    // "The Window"

每个函数在被调用时都会自动取得两个特殊变量: thisarguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量(这一点通过图7-2可以看的清楚)。

不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function() {
    var that = this;
    return function() {
      return that.name;
    };
  }
};

object.getNameFunc()();                    // "My Object"

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

var name = "The Window";

var object = {
  name: "My Object",

  getName: function() {
    return this.name;
  }
};

// this 指向 object
object.getName();          // "My Object"

// 加上了括号,看似在引用一个函数
// 但 (object.getName) 和 object.getName 的定义是相同的
// 所以这行代码与上面的代码无异
(object.getName)();      // "My Object"

// 非严格模式
// 赋值语句会返回 object.getName 的匿名函数
// 相当于将匿名函数在全局环境下运行
(object.getName = object.getName)();          // "The Window"

第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果就返回了 "The Window" 。

内存泄漏

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

function assignHandler() {
  var element = document.getElementById("someElement");
  element.onclick = function() {
    console.log(element.id);
  };
}

以上代码创建了一个作为element元素处理程序的闭包,而这个闭包则又创建了一个循环引用(事件将在第13章讨论)。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少是1。

// 以下修改可以避免这个问题
function assignHandler() {
  var element = document.getElementById("someElement");
  var id = element.id;

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

  element = null;
}

闭包中引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象也仍然会保存一个引用。因此有必要把element变量设置为null

模仿块级作用域

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

function outputNumerbs(cout) {
  for (var i=0; i < cout; i++) {
    console.log(i);
  }
  console.log(i);    // 计数
}

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

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

  var i;             // 重新声明变量
  console.log(i);    // 计数
}

JavaScript从来不会告诉你是否多次声明了同一个变量,遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。

匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为 私有作用域 )的匿名函数的语法如下:

(function() {
  // 这里是块级作用域
  ...
})()
// 常见的代码片段
// 定义了一个函数,然后立即调用它
var someFunction = function() {
  // 这里是块级作用域
  ...
};
someFunction();

那这里如果将函数名也去掉呢?答案是不行,会导致出错。因为JavaScriptfunction关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。(函数表达式可以)

function() {
  // 这里是块级作用域
  ...
}()    // 出错!

要将函数声明转换成函数表达式,只要外面包裹圆括号即可

(function() {
  // 这里是块级作用域
  ...
}())

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

function outputNumbers(cout) {
  // 这里是一个闭包,匿名函数可以访问 cout
  (function () {
    for (var i=0; i < cout; i++) {
      console.log(i);
    }
  })();

  // 在这里调用变量 i 会报错
  console.log(i);                  // throw error
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。

(function() {

  var now = new Date();
  if (now.getMonth() == 0 && now.gettDate() == 1) {
    console.log("Haapy new year!");
  }
})();

这种做法可以减少闭包占用的内存,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

严格来讲,JavaScript中没有私有成员的概念;所有对象属性都是共有的。不过倒是有一个私有变量的概念。

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。

如果在函数内部创建一个闭包,那么闭包通过自己的作用域链可以访问这些私有变量。利用这一点,我们就可以创建用于访问私有变量的公有方法。

有权访问私有变量和私有函数的公有方法称为 特权方法(privileged method) 。有两种创建特权方法的方式:

在构造函数中定义特权方法(静态私有变量)

模块模式

构造函数中定义,基本的模式如下

// 构造函数Person
// 入参 name 是它的私有变量
function Person(name) {
  this.getName = function() {
    return name;
  };

  this.setName = function(value) {
    name = value;
  };
}

var person = new Person("Nicholas");
console.log(person.getName());              // "Nicholas"

person.setName("Greg");
console.log(person.getName());              // "Greg"

这种模式有一个缺点,那就是必须使用构造函数来达到目的,而第6章讨论过,构造函数模式的确定是针对每一个实例都会创建出同样的一组新方法

而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量
(function() {

  // 私有变量
  var privateVariable = 10;

  // 私有函数
  function privateFunction() {
    return false;
  }

  // 构造函数
  // 这里没有使用var操作符,自动创建全局变量
  // 严格模式下不能使用
  MyObject = function() {};

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

公有方法是在原型上定义的,避免了重复创建方法的情况。

需要注意的是,这个模式在定义构造函数时没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。

这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

(function() {

  var name = "";

  Person = function(value) {
    name = value;
  };

  Person.prototype.getName = function() {
    return name;
  };

  Person.prototype.setName = function (value) {
    name = value;
  };
})();

var person1 = new Person("Nicholas");
console.log(person1.getName());                          // "Nicholas"
person1.setName("Greg");
console.log(person1.getName());                          // "Greg"

var person2 = new Person("Michael");
console.log(person1.getName());                          // "Michael"
console.log(person2.getName());                          // "Michael"

这个例子中的Person构造函数与getName() setName() 方法一样,都有权访问私有变量name

name变成了一个静态的、由所有实例共享的属性。

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。

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

模块模式

模块模式通过为单例添加私有变量和特权方法使其得到增强

这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时非常有用

var application = function() {

  // 私有变量和函数
  var components = new Array();

  // 初始化
  components.push(new BaseComponent());

  // 公共
  return {
    getComponentCuont: function() {
      return components.length;
    },

    registerComponent: function(component) {
      if (typeof component == "object") {
        components.push(component)
      }
    }
  };
}();

在WEB应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。

简而言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

以模块模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示他。

增强的模块模式

增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。

如果前述例子中的application对象必须是BaseComponent的实例,可以如下代码

var application = function() {

  // 私有变量和函数
  var components = new Array();

  // 初始化
  components.push(new BaseComponent());

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

  // 公共接口
  app.getComponentCuont = function() {
    return components.length;
  };

  app.registerComponent = function(component) {
    if (typeof component == "object") {
      components.push(component);
    }
  };

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

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

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

相关文章

  • JavaScript高级程序设计》(3读书笔记 4 变量、作用域和内存问题

    摘要:具体说就是执行流进入下列任何一个语句时,作用域链就会得到加长语句的块。如果局部环境中存在着同名的标识符,就不会使用位于父环境中的标识符访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。 基本类型和引用类型的值 ECMAscript变量包含 基本类型值和引用类型值 基本类型值值的是基本数据类型:Undefined, Null, Boolean, Number, String ...

    lidashuang 评论0 收藏0
  • JavaScript高级程序设计》(3读书笔记 1~2

    摘要:表示应该立即下载脚本,但不应妨碍页面中的其他操作可选。表示通过属性指定的代码的字符集。表示脚本可以延迟到文档完全被解析和显示之后再执行。实际上,服务器在传送文件时使用的类型通常是,但在中设置这个值却可能导致脚本被忽略。 第1章 JavaScript 简介 虽然JavaScript和ECMAScript通常被人们用来表达相同的含义,但JavaScript的含义比ECMA-262要多得多...

    Corwien 评论0 收藏0
  • JavaScript高级程序设计》(3读书笔记 9 客户端检测

    摘要:用户代理检测用户代理检测是争议最大的客户端检测技术。第二个要检测是。由于实际的版本号可能会包含数字小数点和字母,所以捕获组中使用了表示非空格的特殊字符。版本号不在后面,而是在后面。除了知道设备,最好还能知道的版本号。 检测Web客户端的手段很多,各有利弊,但不到万不得已就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。一言蔽之,先设计最通用的方案,然后再使用特定...

    ispring 评论0 收藏0
  • JavaScript高级程序设计》(3读书笔记 3

    摘要:本质上是由一组无序名值对组成的。浮点数值的最高精度是位小数,但在进行计算时其精度远远不如证书。例如这是使用基于数值的浮点计算的通病,并非独此一家数值范围。 函数名不能使用关键字(typeof不行但typeOf可以,区分大小写) 标识符就是指变量、函数、属性的名字,或者函数的参数。 第一个字符必须是一个字母、下划线(_)或者一个美元符号($) 其他字符可以是字母、下划线、美元符号或...

    renweihub 评论0 收藏0
  • JavaScript高级程序设计》(3读书笔记 5 引用类型

    摘要:引用类型的值对象是引用类型的一个实例。引用类型是一种数据结构,用于将数据和功能组织在一起。对数组中的每一项运行给定函数,如果该函数对任一项返回,则返回。组零始终代表整个表达式。所以,使用非捕获组较使用捕获组更节省内存。 引用类型的值(对象)是引用类型的一个实例。 引用类型是一种数据结构,用于将数据和功能组织在一起。它同行被称为类,但这种称呼并不妥当,尽管ECMAScript从技术上讲...

    zero 评论0 收藏0

发表评论

0条评论

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