函数表达式(第七章笔记)
函数表达式
定义函数
定义函数的方式有两种
-
函数声明
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 的实例,并且在实例上增加了两个特权方法。
上一篇: react 使用 swiper
下一篇: 基本tcp socket编程