函数表达式(第七章笔记)

定义函数

定义函数的方式有两种

  • 函数声明

    funcName(); // 使用函数声明情况下,可以先写调用语句再写函数声明,涉及到了变量提升与函数提升 有兴趣可以看一下
    function funcName() {
      // 函数体
    }
    // 函数的name为funcName
    
  • 函数表达式

    //funcName() 错误 函数还不存在 依旧是变量提升
    var funcName = function () {
      // 函数体
    };
    

上边这种情况创建的函数叫做匿名函数,因为 function 关键字后边没有标识符,匿名函数的 name 是空字符串。

函数提升的结果会让人意想不到,如:

let isFunc = true;
if (isFunc) {
  function funcName() {
    return 1;
  }
} else {
  function funcName() {
    return 2;
  }
}

不要这样做,这样看似根据变量去创建不同的函数,但实际大多数浏览器会直接返回第二个函数,可以写成这样:

let isFunc = true;
if (isFunc) {
  var funcName = function () {
    return 1;
  };
} else {
  var funcName = function () {
    return 2;
  };
}

递归

递归函数是在一个函数通过名字调用自身的情况下构成的(也就是自己调用自己)。

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

但是如果使用这种方式调用会出现问题:

var anotherGen = gen;
gen = null;
anotherGen(4); //出错 因为函数体内部调用的gen已经是null

arguments.callee 是一个指向正在执行函数的指针,可以使用这个解决上边的问题,但严格模式下被禁用了:

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

可以使用终极解决方案:

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

使用这种方式及时函数赋值给其他变量,函数名 f 依然有效。

闭包

要搞清楚匿名函数和闭包不是一个概念,闭包只是使用了匿名函数实现。闭包是指有权访问另一个函数作用域中的变量的函数。而匿名函数是指没有函数名称的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数:

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;
    }
  };
}
var personAge = createComparisonFunction("age");
personAge({ age: 18 }, { age: 22 }); //-1

实际 createComparisonFunction 内部的匿名函数是在最后执行的,但执行过程中却可以访问 createComparisonFunction 内部的形参 propertyName ,此时就形成了闭包。

为了彻底理解闭包,了解如何创建作用域链以及作用域链有什么作用十分重要。当函数被调用时,会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([Scope])。然后,使用 this、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()时,会创建一个包含 this、arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 this、result 和 compare)在 compare()执行环境的作用域链中则处于第二位。下图展示了包含上述关系的 compare()函数执行时的作用域链。


execution context 是执行上下文的意思 可以理解为 this
scope 作用域 Chain 链
global 全局 variable 变量 object 对象
activation 活动 object 对象

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建 compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用 compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中 compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,都会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数将会包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction()的活动对象。
createComparisonFunction 内部的变量本应该在执行完就销毁掉,但由于内部返回的匿名函数引用了 propertyName 变量,延长了 propertyName 变量的生命周期,所以 propertyName 会一直存留在内存中。

var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
//

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

//创建函数
var compareNames = createComparisonFunction("name");

//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });

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

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

在这里插入图片描述
execution context 是执行上下文的意思 可以理解为 this
scope 作用域 Chain 链
global 全局 variable 变量 object 对象
activation 活动 object 对象

闭包与变量

闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的值

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

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

  return result;
}
var list = createFunctions();
for (var v of list) {
  console.log(v()); // 3 3 3
}

上下两种其实是一个意思,我感觉下面更好理解

function createFunctions() {
  var result = new Array();
  var i = 0;
  for (var index = 0; index < 3; index++) {
    result[i] = function () {
      return i;
    };
    i++;
  }

  return result;
}
var list = createFunctions();
for (var v of list) {
  console.log(v()); // 3 3 3
}

像这样,每次调用返回的匿名函数返回值都是 3,因为调用的时候 createFunctions 内部的 i 的就是 3。
但是我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。

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

  for (var i = 0; i < 3; i++) {
    //匿名函数直接赋值
    result[i] = (function (num) {
      return function () {
        return num;
      };
    })(i);
  }

  return result;
}

var list = createFunctions();
for (var v of list) {
  console.log(v()); // 0 1 2
}

当调用 createFunctions 的时候,result 赋值了一个立即执行函数,返回了一个匿名函数,这次匿名函数中引用的 num 已经不是 createFunctions 中的 i 了,而是赋值时传递给立即执行函数的 num,所以赋值时 i 是几,num 就是几。
前面 result 里面的匿名函数返回的是同一个 i,后面 result 里面的匿名函数返回的是每个立即执行函数里的 num。

关于 this 对象

先理解一下 this 的指向

一般情况:
简单来说函数内部的 this 指向的就是调用它的环境对象,也就是说调用时函数名前面的.或者[]前面的那个对象。 如果没有那就是全局调用的。
特殊情况:

  • 闭包直接两次调用 都指向 window
  • 事件委托 (dom 冒泡触发的事件)指向写着事件的那个标签
  • 使用 new 标签 也就是构造函数 这时候会创建一个对象在函数顶部并在最后 return 回去(当没改写 return 的情况),this 指向顶部的对象。
  • call、apply、bind this 指向第一个参数
  • 箭头函数在定义阶段就决定了 this 的指向,定义函数的上一行,this 指向谁,箭头函数中 this 就指向谁,且不能被 call apply bind 改变
function getThis() {
  return this;
}
getThis(); //window

var obj = {
  getThis: getThis,
};
obj.getThis(); //这里有个. 指向obj

var arr = [getThis];
arr[0](); //这里有个[] 指向arr

new getThis(); //指向一个对象

getThis.call(arr); //call、apply、bind this 指向第一个参数 arr

var func = () => {
  return this;
};
func(); //指向window
正经的解释 this 指向

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

var name = "The Window";

var object = {
  name: "My Object",

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

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

// 为什么闭包指向了window 我的理解是 object.getNameFunc()调用之后 直接返回了一个匿名函数,而匿名函数直接调用前面并没有调用者,如下
// object.getNameFunc()() => (function(){return this.name;})()

在每个函数被调用时,其活动对象都会自动取得两个特殊变量:this 和 arguments。
而如果访问 object 的属性,就需要把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里。如下:

var name = "The Window";

var object = {
  name: "My Object",

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

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

内存泄漏

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

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

以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对 assingHandler()的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。不过,可以通过该写代码来解决,如下:

//防止内存泄露
function assignHandler() {
  var element = document.getElementById("someElement");
  var id = element.id;

  element.onclick = function () {
    alert(id);
  };

  element = null;
}

上面的代码中,通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个应用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域

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

function outputNumbers(count) {
  for (var i = 0; i < count; i++) {}
  var i; //重新声明变量不赋值的话也不会改变结果 这里又涉及到变量提升 会把这句话放到函数顶部
  alert(i); //计数
}

这个函数中定义了一个 for 循环,而变量 i 的初始值被设置为 0。在 Java、C++中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScript 中,变量 i 是定义在 outputNumbers()的活动对象中的,因此从它有定义开始,既可以在函数内部随处访问它。
使用立即执行的匿名函数创造一个作块级作用域(通常称为私用作用域)可以解决这个问题:

function outputNumbers(count) {
  (function () {
    // 匿名函数内部可以获取到外部的count 这就形成了一个闭包
    for (var i = 0; i < count; i++) {
      alert(i);
    }
  })();
  // 这里却获取不到上边立即执行的匿名函数内部变量 i
  alert(i); //导致一个错误!
}
// 下面这种属于函数声明 缺少函数名并且后边不允许跟随小括号
// 而使用小括号把函数包裹起来 就会当函数表达式处理 就像上边那样
// function () {
//   for (var i = 0; i < count; i++) {
//     alert(i);
//   }
// }();

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个有很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。例如:

(function () {
  var now = new Data();
  if (now.getData() === 1) {
    console.log("今天是一号");
  }
})();

这种做法可以减少闭包的内存占用问题,因为是立即执行没有指向函数的引用,执行结束就可以立即销毁。

私有变量

严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。函数内部的变量包括形参、this、arguments、局部变量和在函数内部定义的其它函数
如果在函数内部创建闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。

有两种在对象上创建特权方法的方式。

第一种是在构造函数中定义特权方法,基本模式如下。

function MyObject() {
  //私有变量和私有函数
  var privateVariable = 10;

  function privateFunction() {
    return false;
  }

  //特权方法
  this.publicMethod = function () {
    // 作为闭包 可以访问到 MyObject 内部定义的所有变量与函数
    privateVariable++;
    return privateFunction();
  };
}
var instance = new MyObject();
instance.publicMethod(); //除了使用这个方法 没有任何办法可以直接访问 privateVariable 和 privateFunction 所以被称为特权方法

利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例如:

function Person(name) {
  this.getName = function () {
    // 想不到吧 这也是闭包 不理解的话 想想开头闭包的概念
    return name;
  };

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

var person = new Person("Nihcholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
// 除了这两个方法 没有任何办法可以操作 name

构造函数定义特权方法也有一个缺点,针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。使用这种方法改造上边的代码:

// 这个需求根本不需要闭包 单纯就是为了展示使用闭包定义私有变量并且利用构造函数 吐了吐了。。
var Person; // 这里不写 在严格模式下报错
(function () {
  var name = "";

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

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

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

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

// 由于name只有一个 所有实例的name 都是同一个
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
// 还有个缺点要说 每次操作私有变量 都要往作用域链内部找 查找的越深越耗时

定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。我们可以在声明 MyObject 时使用不 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但是严格模式下将会报错
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript 是以字面量的方式来创建单例对象的,通过为单例添加私有变量和特权方法能够使其得到增强,其语法格式如下:

// 最简单的单例
var singleton = {
  name: value,
  method: function () {
    //这里是方法的代码
  },
};

// 增强版单例
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();

  //假设这里是 初始化 不要关心 new BaseComponent() 是个啥 就是初始化
  components.push(new BaseComponent());

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

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

在 Web 应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的 application 对象。在创建这个对象的过程中,首先声明了一个私有的 components 数组,并向数组中添加了一个 BaseComponent 的新实例(在这里不需要关心 BaseComponent 的代码,我们只是用它来展示初始化操作)。而返回对象的 getComponentCount()和 registerComponent()方法,都是有权访问数组 components 的特权方法。前者只是返回已注册的组件数目,后者用于注册新组件。
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 object 的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没有什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用 instanceof 操作符来检查其对象类型了。

增强的模块模式

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

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(component);
    }
  };

  //返回这个副本
  return app;
})();
// 这时候返回的单例对象就成为了 BaseComponent 的实例,并且在实例上增加了两个特权方法。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值