Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

「6」JavaScript 函数表达式学习笔记 #7

Open
BuptStEve opened this issue Mar 24, 2016 · 0 comments
Open

「6」JavaScript 函数表达式学习笔记 #7

BuptStEve opened this issue Mar 24, 2016 · 0 comments

Comments

@BuptStEve
Copy link
Owner

BuptStEve commented Mar 24, 2016

零、前言
《JavaScript 高级程序设计(第三版)》第7章 函数表达式,学习笔记整理。

主要内容有如下三部分

  • 函数表达式的特征
  • 使用函数实现递归
  • 使用闭包定义私有变量

一、第7章 函数表达式

7.0. 函数定义

在 JavaScript 中定义函数有两种方法:

  • 函数声明
  • 函数表达式

7.0.1. 函数声明

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

// 只在 Firefox、Safari、Chrome 和 Opera 有效
alert(functionName.name); // functionName,函数名称

函数声明一个重要特征就是:函数声明提升(function declaration hoisting),简单来说就是 JS 引擎会在执行阶段之前读取函数声明,这就是我们才能够在函数声明之前就调用它的原因。

7.0.2. 函数表达式

// 有多种形式,以下为最常见的一种,即创建一个匿名函数并将其赋值给变量 functionName
var functionName = function(arg0, arg1, arg2) {
  // 函数体
};

// 只在 Firefox、Safari、Chrome 和 Opera 有效
alert(functionName.name); // 空字符串

当然函数表达式就没有声明提升这种特征了。以下是一个比较常见的坑...

// 千万别这样做!
// 因为有的浏览器会返回 first 的这个 function,而有的浏览器返回的却是第二个
if (true) {
  function foo() {
    return 'first';
  }
} else {
  function foo() {
    return 'second';
  }
}
foo();

// 相反,这样情况,我们要用函数表达式
var foo;

if (true) {
  foo = function() {
    return 'first';
  };
} else {
  foo = function() {
    return 'second';
  };
}
foo();

7.1. 递归

先说个段子:要想理解递归,首先要理解...递归。

说正经的,递归就是函数自己调用自己。

function factorial(num) {
  // 递归结束条件
  if (num <= 1) return 1;

  // 通过在全局 VO 中,找到自身函数的指针后调用自身
  return num * factorial(num-1);
}

var anotherFactorial = factorial;
factorial = null;

// 报错!因为修改了全局 VO 中 factorial 指针的指向(null)
alert(anotherFactorial(4));

这么写主要问题就是递归函数与自身的函数名耦合,一旦修改了原本的函数名,则会导致错误。

这时可以利用 arguments.callee 指针来成功寻找到正在执行的函数。

接下来又有一个坑:arguments 在严格模式下无法使用。

不过我们可以使用命名函数表达式来完美解决:

var factorial = function f(num) {
  // 递归结束条件
  if (num <= 1) return 1;

  // 函数名 f 只在内部作用域里有效
  return num * f(num-1);
};

typeof f; // undefined

7.2. 闭包

先下定义:闭包是指【有权】访问(另一个函数作用域)中的「变量」的「函数」。

  • 闭包首先是一个函数
  • 能力就是有权访问变量
  • 范围在另一个函数作用域内

我们日常在使用 JavaScript 中,在外部函数中定义的内部函数能够访问外部函数中的变量。

所以,最常见的闭包方式就是在一个函数内部创建并返回另一个函数。

function foo(arg0, arg1) {
  return function bar() {
    // 内部的 bar 函数能够访问外部函数 foo 的 arg0 和 arg1 变量。
    return arg0 + arg1;
  }
}

var test = foo('a', 'b'); // test 是一个指针,指向返回的 bar 函数
test();                   // ab,调用闭包后,得到 arg0 + arg1 的值
typeof bar;               // undefined,当然返回的是一个匿名函数

那么内部的 bar 函数是怎么保存外部 foo 函数的两个参数的呢?

  • 首先在浏览器端的 JavaScript 代码,有一个全局的执行环境(Execution Context)即 window 对象。
  • 每个执行环境都有一个对应的变量对象(Variable Object):定义的所有变量和函数都保存其中。
  • 当某个函数被调用时,会创建一个执行环境(EC)及相应的作用域链(Scope Chain)(其实是被推入一个环境栈中,执行之后栈将之弹出)。
  • 作用域链(SC)就是一个指向各个变量对象的指针列表(全局 VO 是其中的最后一个对象)。
  • 一般来说某个执行环境(EC)的所有代码执行完毕后,该环境被销毁,保存其中的所有变量和函数定义也随之销毁。
  • 如果执行环境是一个函数,那么它的变量对象(VO)又叫做活动对象(Activation Object)。
  • 因此在一个函数中解析标识符的过程,类似于上一篇中提到的原型链查找。也是沿着 SC 一级一级地往上找。如果直到全局 VO 还没找到,就会报错。

所以接下来举几个栗子:

1. 普通函数

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

  return 0;
}

var result = compare(5, 10);
      ______________________________________________________________
      |                                                             |
      V                                     _____________________   |           _____________________
 _________________                    |--> |       global VO     |  |     |--->|     compare AO      |
|   compare EC    |                   |    |---------------------|  |     |    |---------------------|
|-------—---------|       _______     |    | compare |      *----|--|     |    | arguments | [5, 10] |
| Scope Chain | *-|----> |  SC   |    |    |---------------------|        |    |---------------------|
|-----------------|      |-------|    |    | result  | undefined |        |    | value1    |    5    |
                         | 1 | *-|----|    |---------------------|        |    |---------------------|
                         |-------|                                        |    | value2    |   10    |
                         | 0 | *-|----------------------------------------|    |---------------------|
                         |-------|

                                           by 灵魂画师...(累死我了)
  • 在创建 compare 函数时,就已经创建一个预先包含全局 VO 的作用域链(保存在内部的 [[Scope]] 属性中)。
  • 在执行 compare 函数时,就创建 EC,然后复制并构建 [[Scope]] 属性中的作用域链 SC
  • 此时创建 compare AO,并将其放入 SC 的顶端。
  • 所以在函数中访问一个变量时,会从作用域顶端(就是 compare AO)开始找起。
  • 所以一般一个函数执行完毕后,因为 AO 被销毁,所以在函数外部无法访问到函数内部的变量。

2. 闭包示例

function createCompFunc(propName) {

  return function(obj1, obj2) {
    var value1 = obj1[propName];
    var value2 = obj2[propName];

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

var compare = createCompFunc("name");                   // 创建函数

var result = compare({name: "steve"}, {name: "young"}); // 调用函数

compare = null;                                         // 解除对匿名函数的引用(释放内存)
          __________________________________________________________________
          |                                         _____________________   |
          |                                |-----> |      global VO      |  |
          |                                |       |---------------------|  |
          |                                |       | createCompFunc | *--|--|
          |                                |       |---------------------|
          V                                |       | result  | undefined |
 ___________________              _______  |       |---------------------|
| createCompFunc EC |      |---> |  SC1  | |        ______________________
|-------—-----------|      |     |-------| | |---> |  createCompFunc AO   |
| Scope Chain |  *--|------|     | 1 | *-|-| |     |----------------------|
|-------------------|            |-------| | |     | arguments | ["name"] |
                                 | 0 | *-|-(-|     |----------------------|
 ________________________        |-------| | |     | propName  |  "name"  |
|  anoymous function EC  |                 | |     |----------------------|
|-------—----------------|        _______  | |      _______________________________
| Scope Chain |    *-----|-----> |  SC2  | | | |-> |      anoymous function AO     |
|------------------------|       |-------| | | |   |-------------------------------|
                                 | 2 | *-|-| | |   | arguments | [{name: "steve"}, |
                                 |-------|   | |   |           |  {name: "young"}] |
                                 | 1 | *-|---| |   |-------------------------------|
                                 |-------|     |   |   obj1    |  {name: "steve"}  |
                                 | 0 | *-|-----|   |-------------------------------|
                                 |-------|         |   obj2    |  {name: "young"}  |
                                                   |-------------------------------|
                                 by 灵魂画师...(累死我了)
  • 如图所示,首先看全局变量对象(global VO),其中有一个指针指向 createCompFunc 函数,还有一个声明提升后还未计算完毕的 result 等变量。
  • 首先执行外部的 createCompFunc 函数,创建了它的 EC,其中有一个作用域链指针指向它的作用域链 SC1。
  • 那么显然 SC1 中会有两个指针,分别指向 createCompFunc AO 和 global VO(这就是在函数内部能够访问到全局变量的原因)。
  • 在 createCompFunc AO 中存放的就是函数内部定义的变量。
  • 最后来看 createCompFunc 函数内部返回的匿名函数,当然它也有自己的执行环境 EC,也有自己的作用域链 SC2。
  • 只不过由于它是 createCompFunc 函数内部的函数,当然它也能访问外部 createCompFunc 函数定义的变量,这正是因为 SC2 中位于第二的指针指向 createCompFunc AO(这就是闭包的原理)。
  • 更为重要的是,在 createCompFunc 函数执行完毕后,因为匿名函数仍然引用着 createCompFunc AO,所以其活动对象不会被销毁(这涉及到内存回收机制)。

从上述讨论我们可以清晰地看出:闭包的原理就是内部的函数仍然引用着外部函数的 AO,使得外部函数的 AO 仍然保存在内存中。所以我们可以通过将闭包设置为 null 来解除对该函数的引用,回收其占用的内存。

7.2.1. 闭包与变量

下面我们来简单讨论下作用域链机制的副作用(坑):闭包只能取得外部函数中任何变量的最后一个值。

先来看一个栗子:

function createFunc() {
  var result = [];

  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i; // 希望保存不同的 i,但是最后 i 都是 10
    };
  }

  return result;
}

var test = createFunc();
alert(test[0]()); // 10

其实很好理解,因为闭包在 SC 中保存的是一个指针而已,外部函数执行完毕后 AO 中的变量自然更新为最后一个值啦╮(╯▽╰)╭。

但是我们可以创建另一个立即执行的匿名函数强制让闭包的行为符合预期:

function createFunc() {
  var result = [];

  for (var i = 0; i < 10; i++) {
    result[i] = function(num) {
      return function() {
        return num;
      }
    }(i);
  }

  return result;
}

var test = createFunc();
alert(test[0]()); // 0
  • 在这里我们相当于有3层函数。
  • 中间那层的匿名函数有一个参数 num,我们将 i 传入立即执行。
  • 因为简单类型是按值传递的,所以中间那层的匿名函数的 AO 中 num 保存的是不同的 i。
  • 而最内层的匿名函数读取的正是中间层匿名函数的 AO。

7.2.2. 关于 this 对象

我们知道,this 是在运行时根据函数的执行环境动态绑定的:

  • 在全局函数中,this 等于 window。
  • 而函数被作为某个对象的方法调用时,this 又指向那个对象。
  • 使用 call() 和 apply() 方法时,this 又会指向传入的那个对象(还有 bind())。

不过,匿名函数的执行环境具有全局性,因此内部的 this 通常指向 window,见下例:

var name = "The Window";

var object = {
  name: "My Object",

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

alert(object.getNameFunc()()); // The Window (非严格模式下),此处有两个括号,因为 object.getNameFunc() 是一个匿名函数,后一个括号是匿名函数调用。

为什么匿名函数没有取得其包含作用域(或外部作用域)的 this 对象呢?

前面曾经提到过,每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。而内部函数在搜索这两个变量时,只会搜索到其 AO 为止,所以永远不能直接访问到外部函数中的 this 和 arguments。

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

var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function() {
    var that = this; // 使用 that 保存外部函数的 this(防止被内部函数的 this 屏蔽)

    return function() {
      return that.name; // 由于访问的是内部 AO 中没有的变量 that,所以在 SC 中外部的 AO 上搜索,得到外部函数的 this。
    };
  }
};

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

接下来再看几个特殊的栗子:

var name = "The Window";

var object = {
  name: "My Object",

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

object.getName();   // My Object,很好理解 this 就是指向 object
(object.getName)(); // My Object,将函数包了起来,但还是通过 object.getName 调用,this 还是指向 object

(object.getName = object.getName)(); // The Window(非严格模式下),看起来很奇怪,将函数 getName 赋值为 getName,再调用赋值后的结果
// 因为赋值表达式操作的是 getName 函数本身,所以 this 的值没有得到维持,调用时指向了 widnow。

7.2.3. 内存泄漏

因为 IE9 之前对于 JScript 对象和 COM 对象使用不同的垃圾收集机制。所以如果闭包的作用域中保存一个 HTML 元素,那么该元素将无法被销毁╮(╯▽╰)╭。

7.3. 模仿块级作用域

我们都知道在 ES6 之前是木有块级作用域的╮(╯▽╰)╭,在块语句中定义的变量实际上是定义在函数 AO 上的。

如果实在需要块级作用域,可以通过立即执行函数进行模拟。

// 这只是其中一种写法...重点是不要使用函数声明,这样由于声明提升会报错。
(function() {
  // 这里是块级作用域
})();

在一个大型程序中过多的全局变量和函数很容易造成命名冲突,这样可以有效减少在全局作用域中添加变量和函数。也可以说是模块化的基础。ps jQuery 源码中最外层函数就是这样的一个立即执行函数...

7.4. 私有变量

严格来说,JavaScript 中没有私有成员的概念...╮(╯▽╰)╭,不过在任何函数中定义的变量,外部都无法访问到,可以认为是私有变量。

私有变量包括:

  • 函数的参数
  • 局部变量
  • 内部定义的其他函数

那么如果我们需要访问这些私有变量该怎么办呢?
在此就要引入一个概念:特权方法(privileged method)有权访问私有变量和私有函数的公有方法。

这个概念是不是和闭包炒鸡像!?

其实有两种在对象上创建特权方法的方式,第一种就是利用闭包在构造函数中定义特权方法。基本模式如下:

function MyObject() {

  // 私有变量和函数
  var privateVar = 10;

  function privateFunc() {
    return false;
  }

  // 特权方法
  this.publicMethod = function() {
    privateVar++;
    return privateFunc();
  };
}

但是,这样定义特权方法有一个问题:必须使用构造函数模式。而之前已经讨论过了构造函数会为每个实例都创建一组新方法,浪费内存,而接下来介绍的第二种方法,使用静态私有变量就可以避免这个问题。

7.4.1. 静态私有变量

基本思想就是:在私有作用域(立即执行函数)中定义私有变量或函数,并在内部通过函数表达式定义构造函数和它的公有方法。基本模式如下:

(function() {

  // 私有变量和私有函数
  var privateVar = 10;

  function privateFunc() {
    return false;
  }

  // 构造函数
  MyObject = function() {}; // 没有使用 var,所以是在 window 上创建(所以是非严格模式下)。

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

})();

显然,通过上一篇对于对象继承的讨论我们知道:定义在原型对象上的属性和方法是所有实例共享的,而这些方法(如 publicMethod)所操作的对象,即私有变量和函数也是同一个。所以通过 MyObject 构造函数创造的实例都有权访问私有变量(而且是同一个),这就是静态私有变量。

7.4.2. 模块模式

模块模式(module pattern)是为「单例」创建私有变量和特权方法。

那么问题来了,单例是啥?

单例(singleton)指的就是只有一个实例的对象。比如 JavaScript 中就是以对象字面量来创建单例对象的。

var singleton = {
  name : value,
  method: function() {
    // 方法的代码
  }
};

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

var singleton = function() {

  // 私有变量和私有函数
  var privateVar = 10;

  function privateFunc() {
    return false;
  }

  // 公有/特权方法
  return {

    publicProperty: true,

    publicMethod: function() {
      privateVar++;
      return privateFunc();
    };
  };
}();

如上面代码所示,模块模式就是使用了一个返回对象的匿名函数:

  • 在函数内部定义了私有变量和函数
  • 然后将对象字面量返回。
  • 因为对象是函数内部定义的,所以它的方法是一个闭包。

模块模式的应用场景主要是在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。

7.4.3. 增强的模块模式

应用场景:单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其进行加强的情况。

var singleton = function() {

  // 私有变量和私有函数
  var privateVar = 10;

  function privateFunc() {
    return false;
  }

  // 创建对象
  var object = new CustomType();

  // 添加公有/特权属性和方法
  object.publicProperty = true;

  object.publicMethod = function() {
    privateVar++;
    return privateFunc();
  };

  return object;
}();

7.5. 小结

1. 在 JavaScript 中,使用函数表达式可以无须对函数命名,从而实现动态编程,还有强大的匿名函数,以下是函数表达式的特点:

  • 函数声明必须要有名字,还会声明提升,而函数表达式不需要,没有名字的函数表达式又叫匿名函数
  • 在递归函数中使用函数名调用自身可能会出现问题,如函数名发生了变化。
  • 在递归函数中要掌握调用函数自身的技巧,如 arguments.callee 等。

2. 闭包:在外部函数中又定义了一个内部函数,这个内部函数有权访问外部函数的所有变量,原理如下:

  • 在 JavaScript 引擎执行时,闭包的 SC 包含了自己的 AO、外部函数的 AO 还有全局 VO。
  • 通常,函数的作用域和所有变量会在函数执行后被销毁。
  • 但是,当函数返回了一个闭包时,外部函数的 AO 会一直保存在内存中,直到解除闭包的引用,如设为 null。

3. 使用闭包可以模仿块级作用域:

  • 立即执行函数既可以执行其中的代码,又不会在内存中留下对该函数的引用。
  • 所以函数内部的所有变量都会被立即销毁,除非将某些变量赋值给外部作用域中的变量。

4. 闭包还可以用于在对象中创建私有变量:

  • 函数内部的变量、方法,外部无法访问。
  • 但通过闭包可以实现公有方法,即访问在包含作用域中定义的变量。
  • 有权访问私有变量的公有方法叫做特权方法。
  • 可以使用构造函数模式、原型模式来实现自定义类型的特权方法。
  • 也可以使用模块模式、增强模块模式来实现单例的特权方法。

综上:JavaScript 中函数表达式和闭包都是很给力的特性。不过,因为创建闭包必须维护额外的作用域,所以过度使用可能会占用大量内存。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant