函数的定义与调用
函数的定义
函数的定义大体上可以分为3种,分别是函数声明、函数表达式和Function构造函数。
1. 函数声明
函数声明是直接使用function关键字接一个函数名,示例如下:
// 函数声明式function sum(num1, num2) {return num1 + num2;}
2. 函数表达式
函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数,示例如下:
// 该函数表达式没有名称,属于匿名函数表达式。匿名函数的this是全局对象windowvar sum = function (num1, num2) {console.log(this) // windowreturn num1 + num2;};// 具有函数名的函数表达式var sum = function foo(num1, num2) {return num1 + num2;};console.log(foo(1,2)) // ReferenceError: foo is not defined// 其中foo是函数名称,它实际是函数内部的一个局部变量,在函数外部是无法直接调用的
应用场景:
(1)函数递归
// 函数声明function fibonacci(num) {if (num === 1 || num === 2) {return 1};return fibonacci(num - 2) + fibonacci(num - 1)}// 函数表达式var fibonacci = function (num) {if (num === 1 || num === 2) {return 1};return fibonacci(num - 2) + fibonacci(num - 1)}
(2)代码模块化
在ES6以前,JavaScript中是没有块级作用域的,但是我们可以通过函数表达式来间接地实现模块化,将特定的模块代码封装在一个函数中,只对外暴露接口,使用者也不用关心具体细节,这样做可以很好地避免全局环境的污染。
// 创建一个立即执行的匿名函数表达式,返回的是一个对象// 使用者只需要调用getName()函数和setName()函数,而不用关心person私有的_name属性var person = (function () {var _name = ""return {getName: function () {return _name},setName: function (newName) {_name = newName}}}())person.setName('kingx')person.getName() // 'kingx'
3. Function()构造函数
使用new操作符,调用Function()构造函数,传入对应的参数,也可以定义一个函数,示例如下:
// 其中的参数,除了最后一个参数是执行的函数体,其他参数都是函数的形参。var add = new Function("a", "b", "return a + b");
相比于函数声明和函数表达式这两种方式,Function()构造函数的使用比较少,主要有以下两个原因:
- Function()构造函数每次执行时,都会解析函数主体,并创建一个新的函数对象,所以当在一个循环或者频繁执行的函数中调用Function()构造函数时,效率是非常低的。
- 所以在一个函数A内部调用Function()构造函数时,其中的函数体并不能访问到函数A中的局部变量,而只能访问到全局变量。
var y = 'global'; // 全局环境定义的y值function constructFunction() {var y = 'local'; // 局部环境定义的y值return new Function('return y'); // 无法获取局部环境定义的值}console.log(constructFunction()()); // 输出'global'
4. 函数声明与函数表达式的区别
(1)函数名称。函数声明必须设置函数名称,这个函数名称相当于一个变量,以后函数的调用也会通过这个变量进行。函数表达式是可选的,可以定义一个匿名函数表达式,也可以定义一个具名函数表达式。
(2)函数提升。对于函数声明,存在函数提升,所以即使函数的调用在函数的声明之前,仍然可以正常执行。对于函数表达式,不存在函数提升,所以在函数定义之前,不能对其进行调用,否则会抛出异常。console.log(add(1, 2)); // "3"console.log(sub(5, 3)); // Uncaught TypeError: sub is not a function// 函数声明function add(a1, a2) {return a1 + a2;}// 函数表达式var sub = function (a1, a2) {return a1 - a2;};
函数的调用
函数的调用存在5种模式,分别是函数调用模式,方法调用模式,构造器调用模式,call()、apply()函数调用模式,匿名函数调用模式。1. 函数调用模式
函数调用模式是通过函数声明或者函数表达式的方式定义函数,然后直接通过函数名调用的模式。// 函数声明function add(a1, a2) {return a1 + a2;}// 函数表达式var sub = function (a1, a2) {return a1 - a2;};add(1, 3);sub(4, 1);
2. 方法调用模式
方法调用模式会优先定义一个对象obj,然后在对象内部定义值为函数的属性property,通过对象obj.property()来进行函数的调用。匿名函数的this是全局对象window,所以会有回调地狱 ```javascript var obj = { name: ‘kingx’, // 定义getName属性,值为一个函数 getName: function () { return this.name; } }; obj.getName(); // 通过对象进行调用 obj‘getName’;
// 如果在某个方法中返回的是函数对象本身this,那么可以利用链式调用原理进行连续的函数调用。 var obj2 = { name: ‘kingx’, getName: function () { console.log(this.name) }, setName: function (name) { this.name = name return this // 在函数内部返回函数对象本身 }, test: function () { // 用_this保存obj中的this var _this = this return function () { // 匿名函数 console.log(this) // window return _this.name } } } obj2.setName(‘kingx2’).getName(); // 链式函数调用
<a name="Dd9Vt"></a>### 3. 构造器调用模式构造器调用模式会定义一个函数,在函数中定义实例属性,在原型上定义函数,然后通过new操作符生成函数的实例,再通过实例调用原型上定义的函数。```javascript// 定义函数对象function Person(name) {this.name = name;}// 原型上定义函数Person.prototype.getName = function () {return this.name;};// 通过new操作符生成实例var p = new Person('kingx');// 通过实例进行函数的调用p.getName();
4. call()、apply()、bind()函数调用模式
call()、apply()、bind()的比较:
相同之处:通过call()函数、apply()函数、bind()函数可以改变函数执行的主体,改变this的指向,使得某些不具有特定函数的对象可以直接调用该特定函数。
不同之处:
- 关于函数立即执行。call()函数与apply()函数在执行后会立即调用前面的函数,而bind()函数不会立即调用,它会返回一个新的函数,可以在任何时候进行调用。要想bind函数立即执行也可以直接
**function.bind(obj, arg1, arg2, ···)()** - 关于参数传递。第一个参数都表示将要改变的函数执行主体,即this的指向;第二个参数call()函数与bind()一个一个列出来的参数,而apply()函数是一个数组(类似于展开运算符,将数组转化为参数),并且如果apply()函数第二个参数不是一个有效的数组或者类数组arguments对象,则会抛出一个TypeError异常。 ```javascript // 定义一个函数 function sum(num1, num2) { return num1 + num2; } // 定义一个对象 var person = {};
sum.call(person, 1, 2); sum.apply(person, [1, 2]); let newFunc = sum.bind(person, 1, 2) newFunc() // 3 // 或者 sum.bind(person, 1, 2)() // 3
**应用**:1. 求数组中的最大项和最小项```javascriptvar arr = [3, 5, 7, 2, 9, 11];// 求数组中的最大值console.log(Math.max.apply(null, arr)); // 11// 求数组中的最小值console.log(Math.min.apply(null, arr)); // 2
- 类数组对象转换为数组对象 ```javascript // 任意个数字的求和 function sum() { // arguments是一个类数组对象,自身不能直接调用数组的方法,将传递的参数转换为数组 var arr = Array.prototype.slice.call(arguments); // 调用数组的reduce()函数 return arr.reduce(function (pre, cur) { return pre + cur; }, 0) }
sum(1, 2); // 3 sum(1, 2, 3); // 6 sum(1, 2, 3, 4); // 10
3. 执行匿名函数场景:有一个数组,数组中的每个元素是一个对象,对象是由不同的属性构成,现在我们想要调用一个函数,输出每个对象的各个属性值。```javascriptvar animals = [{ species: 'Lion', name: 'King' },{ species: 'Whale', name: 'Fail' }]for (var i = 0; i < animals.length; i++) {(function (i) {this.print = function () {console.log('#' + i + ' ' + this.species + ': ' + this.name)}this.print()}).call(animals[i], i)}
- 用于继承 ```javascript // 构造继承用到call()函数
// 父类 function Animal(age) { this.age = age this.sleep = function () { return this.name + ‘正在睡觉!’ } } // 子类 function Cat(name, age) { // 使用call()函数实现继承 Animal.call(this, age) this.name = name || ‘tom’ }
var cat = new Cat(‘tony’, 11) console.log(cat.sleep()) // tony正在睡觉! console.log(cat.age) // 11
5. bind()函数配合setTimeout(不能用call、apply,会立即执行)```javascriptfunction LateBloomer() {this.petalCount = Math.ceil(Math.random() * 12) + 1}LateBloomer.prototype.bloom = function () {// setTimeout第一个参数接受的函数是匿名函数,this指向window,所以用bind改变this指向setTimeout(this.declare.bind(this), 1000)// 或者 var that = this// setTimeout(function () {// that.declare()// }, 1000)// 但是setTimeout(this.declare, 1000) 则会undefined}LateBloomer.prototype.declare = function () {console.log('petalCount:' + this.petalCount)}var flower = new LateBloomer()flower.bloom() // 1秒后,调用declare()函数
5. 匿名函数调用模式
匿名函数,顾名思义就是没有函数名称的函数。
// 一种是通过匿名函数表达式var sum = function(num1, num2){return num1 + num2;};sum(1, 2);// 函数表达式后跟小括号()表示的是函数立即执行var sum = function (num1, num2) {return num1 + num2;}(1, 2);console.log(sum); // 3// 另一种是使用小括号()将匿名函数括起来(function (num1, num2) {return num1 + num2;})(1, 2); // 3
变量提升与函数提升
变量提升
只提升变量名,不提升变量值
会产生提升的变量必须是通过var关键字定义的,而不通过var关键字定义的全局变量是不会产生变量提升的。
(function () {console.log(v) // Uncaught ReferenceError: v is not definedv = 'Hello JavaScript'})();
并且变量提升不受if语句影响(即if语句为false,也会进去执行解析变量声明)
(function foo() {if (a) {var a = 10}console.log(a) // undefined})()
函数提升
函数声明会进行函数提示提升,会将整个函数体一起进行提升,包括里面的执行逻辑。函数表达式,是不会进行函数提升的
show() // 你好var show// 函数声明,会被提升function show() {console.log('你好')}// 函数表达式,不会被提升show = function () {console.log('hello')}
并且在函数内,函数声明提升不受return语句的影响(即在return语句之后声明也会执行解析,进行函数提示)
function foo() {function bar() {return 3}console.log(test()) // 函数声明会提前,不受return语句影响return bar()function bar() { // 所以后一个bar()覆盖前一个bar()return 8}function test() {return '函数声明会提前'}}console.log(foo()) // 8
变量提升和函数提升优先级:变量提升的优先级要比函数提升的优先级高
fn()function fn() {console.log(typeof foo) // function// 变量提升var foo = 'variable'// 函数提升function foo() {return 'function'}console.log(typeof foo) // string}// 改写相当于:fn()function fn() {// 变量提升至函数顶部var foo// 函数提升,但是优先级低,出现在变量声明后面,则foo是一个函数function foo() {return 'function'}console.log(typeof foo) // functionfoo = 'variable' // 变量赋值console.log(typeof foo) // string}
自执行函数
自执行函数即函数定义和函数调用的行为先后连续产生。
function (x) {alert(x)} (5) // 抛出异常,Uncaught SyntaxError: Unexpected token (var aa = function (x) { // 匿名函数表达式console.log(x)}(1) // 1true && function (x) {console.log(x)}(2) // 20, function (x) {console.log(x)}(3) // 3!function (x) {console.log(x)}(4) // 4~function (x) {console.log(x)}(5); // 5-function (x) {console.log(x)}(6); // 6+function (x) {console.log(x)}(7) // 7new function () {console.log(8) // 8}new function (x) {console.log(x)}(9) // 9
函数参数
实参形参
当实参是基本数据类型的值时:实际是将实参的值复制一份传递给形参,在函数运行结束时形参被释放,而实参中的值不会变化。
当实参是引用类型的值时:实际是将实参的内存地址传递给形参,即实参和形参都指向相同的内存地址,此时形参可以修改实参的值,但是不能修改实参的内存地址。
var arg = { name: 'kingx' } // 定义一个实参arg为一个对象function fn(param) {param.name = 'kingx2' // 修改形参param的属性值,此时形参param与实参arg指向的是同一个内存地址param = {} // 将形参param指向了一个新的内存地址,但是这并不会影响实参arg的值}fn(arg)console.log(arg) // {name: "kingx2"}
由于JavaScript是一门弱类型的语言,函数参数在遵循上述规则的基础上,还具有以下几个特性:
- 函数可以不用定义形参,可以在函数体中通过arguments对象获取传递的实参并进行处理。
- 在函数定义了形参的情况下,传递的实参与形参的个数并不需要相同,实参与形参会从前到后匹配,未匹配到的形参被当作undefined处理。
- 实参并不需要与形参的数据类型一致,因为形参的数据类型只有在执行期间才能确定,并且还存在隐式数据类型的转换。
由于这些特点的存在,函数参数的处理非常灵活,其中最关键的一点是,JavaScript为函数增加了一个内置的arguments对象。
arguments对象的性质
介绍:**arguments**对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构(除了具有length属性外,不具有数组的一些常用方法)。
性质:
1. 函数外部无法访问**arguments**对象只能在函数内部使用,无法在函数外部访问到arguments对象。
同时**arguments**对象存在于函数级作用域中,一个函数无法直接获取另一个函数的arguments对象。
console.log(typeof arguments) // undefinedfunction foo() {console.log(arguments.length) // 3function foo2() {console.log(arguments.length) // 0}foo2()}foo(1, 2, 3)
2. 可通过索引访问**arguments**对象是一个类数组结构,可以通过索引访问,每一项表示对应传递的实参值,如果该项索引值不存在,则会返回“undefined”。
function sum(num1, num2) {console.log(arguments[0]) // 3console.log(arguments[1]) // 4console.log(arguments[2]) // undefined}sum(3, 4)
3. 由实参决定**arguments**对象的值由实参决定,而不是由定义的形参决定,形参与arguments对象占用独立的内存空间。
arguments对象与形参之间的关系:
- arguments对象的length属性在函数调用的时候就已经确定,不会随着函数的处理而改变。
- 指定的形参在传递实参的情况下,arguments对象与形参值相同,并且可以相互改变。
- 指定的形参在未传递实参的情况下,arguments对象与形参值不能相互改变,并且arguments对象对应索引值返回“undefined”。 ```javascript foo(1, 2)
function foo(a, b, c) { console.log(arguments.length) // 2
// 在形参a、b都传递了实参的情况下,对应的arguments[0]与arguments[1]与a和b相互影响 arguments[0] = 11 console.log(a) // 11 console.log(arguments[1]) // 2 b = 12 console.log(arguments[1]) // 12
// 形参c未传递实参,对arguments[2]值的设置不会影响到c值,对c值的设置也不会影响到arguments[2] arguments[2] = 3 console.log(c) // undefined
c = 13 // 将形参赋值 console.log(arguments[2]) // 3
console.log(arguments.length) // 2 arguments对象的length属性是由实际传递的参数个数决定的 }
**4. 特殊的arguments.callee属性**<br />arguments对象有一个很特殊的属性callee,表示的是当前正在执行的函数,在比较时是严格相等的。```javascriptfoo()function foo() {console.log(arguments.callee === foo) // true}
通过arguments.callee属性获取到函数对象后,可以直接传递参数重新进行函数的调用,这个属性在匿名的递归函数中非常有用。
function create() {return function (n) {if (n <= 1)return 1return n * arguments.callee(n - 1)}}var result = create()(5) // returns 120 (5 * 4 * 3 * 2 * 1)
在上面的代码中,create()函数返回一个匿名函数,在匿名函数内部需要对自身进行调用,因为匿名函数没有函数名称,所以只能通过arguments.callee属性获取函数自身,同时传递参数进行函数调用。
尽管arguments.callee属性可以用于获取函数本身去做递归调用,但是我们并不推荐广泛使用arguments.callee属性,其中有一个主要原因是使用arguments.callee属性后会改变函数内部的this值。
var sillyFunction = function (recursed) {if (!recursed) {console.log(this) // Window {}return arguments.callee(true)}console.log(this) // Arguments {}}sillyFunction()
如果需要在函数内部进行递归调用,推荐使用函数声明或者使用函数表达式,给函数一个明确的函数名。
arguments对象的应用
1. 实参的个数判断
定义一个函数,明确要求在调用时只能传递3个参数,如果传递的参数个数不等于3,则直接抛出异常。
function f(x, y, z) {// 检查传递的参数个数是否正确if (arguments.length !== 3) {throw new Error("期望传递的参数个数为3,实际传递个数为" + arguments.length)}// ...do something}f(1, 2) // Uncaught Error: 期望传递的参数个数为3,实际传递个数为2
2. 任意个数的参数处理
定义一个函数,该函数只会特定处理传递的前几个参数,对于后面的参数不论传递多少个都会统一处理。例如,定义一个函数,需要将多个字符串使用分隔符相连,并返回一个结果字符串。此时第一个参数表示的是分隔符,而后面的所有参数表示待相连的字符串,我们并不关心后面待连接的字符串有多少个,通过arguments对象统一处理即可。
function joinStr(seperator) {// arguments对象是一个类数组结构,可以通过call()函数间接调用slice()函数,得到一个数组var strArr = Array.prototype.slice.call(arguments, 1)// strArr数组直接调用join()函数return strArr.join(seperator)}joinStr('-', 'orange', 'apple', 'banana') // orange-apple-bananajoinStr(',', 'orange', 'apple', 'banana') // orange,apple,banana
3. 模拟函数重载
函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的函数。
我们都知道在JavaScript中是没有函数重载的,主要有以下几点原因:
- JavaScript是一门弱类型的语言,变量只有在使用时才能确定数据类型,通过形参是无法确定数据类型的。
- 无法通过函数的参数个数来指定调用不同的函数,函数的参数个数是在函数调用时才确定下来的。
- 使用函数声明定义的具有相同名称的函数,后者会覆盖前者。 ```javascript function sum(num1, num2) { return num1 + num2 }
function sum(num1, num2, num3) { return num1 + num2 + num3 } // 后定义的sum()函数会覆盖前一个定义的sum() sum(1, 2) // NaN 实际执行的是1 + 2 + undefined = NaN sum(1, 2, 3) // 6
模拟函数重载:```javascriptfunction sum() {// 通过call()函数间接调用数组的slice()函数得到函数参数的数组var arr = Array.prototype.slice.call(arguments)// 调用数组的reduce()函数进行多个值的求和return arr.reduce(function (pre, cur) {return pre + cur}, 0)}sum(1, 2) // 3sum(1, 2, 3) // 6sum(1, 2, 3, 4) // 10
构造函数
构造函数与普通函数区别
- 构造函数的函数名的第一个字母通常会大写。
- 在函数体内部使用this关键字,表示要生成的对象实例,构造函数并不会显式地返回任何值,而是默认返回“this”。
- 作为构造函数调用时,必须与new操作符配合使用,如果不使用new操作符,则只是一个普通函数,普通函数内部的this会指向window。 ```javascript function Person(name) { console.log(this); // Person {} this.name = name this.sayName = function () { alert(this.name) } } var p = new Person(‘kingx’) console.log(p) // Person {name: “kingx”, sayName: ƒ} Person === Person.prototype.constructor // true
Person(‘kingx’) // 不用new操作符,当作普通函数调用 window.sayName() // ‘kingx’
<a name="g3Mgl"></a>## this使用详解**结论**:在JavaScript中,`**this**`永远指向函数的调用者。<br />1. this指向全局对象<br />当函数没有所属对象而直接调用时,this指向的是全局对象```javascriptvar value = 10;var obj = {value: 100,method: function () {var foo = function () { // 匿名函数表达式,console.log(this.value); // 10console.log(this); // Window对象};foo();return this.value;}};
- this指向所属对象
console.log(obj.method()); // 100
- this指向对象实例
4. this指向call()函数、apply()函数、bind()函数调用后重新绑定的对象// 全局变量var number = 10function Person() {// 复写全局变量number = 20// 实例变量this.number = 30}// 原型函数Person.prototype.getNumber = function () {return this.number}// 通过new操作符获取对象的实例var p = new Person()console.log(p.getNumber()) // 30
使用call()函数、apply()函数、bind()函数都会改变this的指向,call()函数、apply()函数在改变函数的执行主体后,会立即调用该函数;而bind()函数在改变函数的执行主体后,并没有立即调用,而是可以在任何时候调用: ```javascript // 全局变量 var value = 10 var obj = { value: 20 } // 全局匿名函数表达式 var method = function () { console.log(this.value) }
method() // 10 method.call(obj) // 20 method.apply(obj) // 20
var newMethod = method.bind(obj) newMethod() // 20
在处理DOM事件处理程序中的this时,call()函数、apply()函数、bind()函数显得尤为有用:```javascriptvar user = {data: [{ name: "kingx1", age: 11 },{ name: "kingx2", age: 12 }],clickHandler: function (event) {// 随机生成整数0或1var randomNum = ((Math.random() * 2 | 0) + 1) - 1// 从data数组里随机获取name属性和age属性,并输出console.log(this.data[randomNum].name + " " + this.data[randomNum].age)}}var button = document.getElementById('btn')// this指向的是button对象,而不是user对象,而button对象没有data属性,为undefined,从而抛出异常。button.onclick = user.clickHandler // TypeError: Cannot read property '1' of undefinedbutton.onclick = user.clickHandler.bind(user); // kingx2 43 kingx1 37
5. 闭包中的this
函数的this变量只能被自身访问,其内部函数无法访问。因此在遇到闭包时,闭包内部的this关键字无法访问到外部函数的this变量。
var user = {sport: 'basketball',data: [{ name: "kingx1", age: 11 },{ name: "kingx2", age: 12 }],clickHandler: function () {// 此时的this指向的是user对象var _this = thisthis.data.forEach(function (person) { // forEach循环实际是一个匿名函数console.log(this) // [object Window]console.log(_this) // user对象console.log(person.name + ' is playing ' + this.sport)})}}user.clickHandler()
6. Vuex解决this指向丢失
class Store {constructor(options = {}) {// 构造器中的this指向实例// 实例属性this._actions = Object.create(null);// 缓存thisconst store = this// 因为this指向实例,所以可以通过原型链找到dispatch,commit这两个方法const {dispatch,commit} = this// 在实例上绑定了两个方法dispatch、commit,解决了实例上dispatch、commit的this指向问题this.dispatch = function boundDispatch(type, payload) {console.log('我是实例上的dispatch');return dispatch.call(store, type, payload)}this.commit = function boundCommit(type, payload, options) {return commit.call(store, type, payload, options)}}// 原型方法,绑在类Store原型对象上,供实例使用。dispatch() {// 类中的方法默认开启了局部的严格模式,被实例调用this就指向实例,被全局调用this为undefined// 但是在constructor中被调用时,被改变了this指向console.log('我是原型上的dispatch', this);}commit() {console.log('commit', this);}// 原型属性 (测试用)test = 1}const store = new Store();store.dispatch(); //通过实例调用,输出结果 this 是什么呢?const {dispatch,commit} = store;dispatch(); // 在全局环境中直接调用从实例身上解构出来的方法(不是调用实例上的原型方法dispatch)commit();
// 简化版本:使用bind关键字重新绑定了getName()函数在调用时内部的this,使其指向实例pclass Person {constructor(name) {this.name = name;// 构造器中的this指向当前实例this.getName = this.getName.bind(this);// 或者用箭头函数// 箭头函数内部的this总是指向定义时所在的对象。箭头函数位于构造函数内部,它定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。this.getName = () => this;}getName() {return this.name;}}const p = new Person('king');let { getName } = p;getName(); // king
加深对this的理解: 这题目太🐂了
function f(k) {this.m = k;return this;}// 先执行f(1),因为f()函数的调用没有所属对象,所以this指向window,此时window.m=1,返回window// 再将返回值“window”赋值给变量m,因为m是全局变量,所以window.m=windowvar m = f(1); // window// 先执行f(2),此时window.m=2,覆盖了window.m=window,最终window.m=2// 再将返回值“window”赋值给全局变量n,最终window.n=windowvar n = f(2); // windowconsole.log(m.m); // undefined m.m=(window.m).m,此时window.m=2即2.m,console.log(n.m); // 2 n.m=(window.n).m=window.m=2
闭包
执行上下文环境
JavaScript每段代码的执行都会存在于一个执行上下文环境中,而任何一个执行上下文环境都会存在于整体的执行上下文环境中。根据栈先进后出的特点,全局环境产生的执行上下文环境会最先压入栈中,存在于栈底。当新的函数进行调用时,会产生的新的执行上下文环境,也会压入栈中。当函数调用完成后,这个上下文环境及其中的数据都会被销毁,并弹出栈,从而进入之前的执行上下文环境中。处于活跃状态的执行上下文环境只能同时有一个:
var a = 10 // 1.进入全局执行上下文环境var fn = function (x) {var c = 10console.log(c + x)}var bar = function (y) {var b = 5fn(y + b) // 3.进入fn()函数执行上下文环境}bar(20) // 2.进入bar()函数执行上下文环境
| 从第1行代码开始,进入全局执行上下文环境,此时执行上下文环境中只存在全局执行上下文环境。 | ![]() |
|---|---|
| 当代码执行到第10行时,调用bar()函数,进入bar()函数执行上下文环境中。 | ![]() |
| 进入bar()函数中,执行到第8行时,调用fn()函数,进入fn()函数执行上下文环境中。 | ![]() |
| 进入fn()函数中,执行完第5行代码后,fn()函数执行上下文环境将会被销毁,从而弹出栈。 | ![]() |
| fn()函数执行上下文环境被销毁后,回到bar()函数执行上下文环境中,执行完第9行代码后,bar()函数执行上下文环境也将被销毁,从而弹出栈。 | ![]() |
| 最后全局上下文环境执行完毕,栈被清空,流程执行结束。 |
有另外一种情况,虽然代码执行完毕,但执行上下文环境却被无法干净地销毁,这就是闭包。
闭包的概念
官方通用的解释:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。(函数套函数,外部函数返回内部函数,内部函数访问外部函数的变量)
特点:
- 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
- 闭包作为一个函数返回时,其执行上下文环境不会被销毁,仍处于执行上下文环境中。
| 代码开始执行后,生成全局上下文环境,并将其压入栈中 |function fn() {var max = 10return function bar(x) {if (x > max) {console.log(x)}}}var f1 = fn()f1(11) // 11
|
| —- | —- |
| 代码执行到第9行时,进入fn()函数中,生成fn()函数执行上下文环境,并将其压入栈中。fn函数执行完毕返回一个bar()函数,并将其赋给变量f1。 |
|
| 当代码执行到第10行时,调用f1()函数,因为f1()函数中包含了对max变量的引用,而max变量是存在于外部函数fn()中的,所以fn()函数执行上下文环境并不会被直接销毁,依然存在于执行上下文环境中。 |
|
| 等到第10行代码执行结束后,bar()函数执行完毕,bar()函数执行上下文环境才会被销毁,同时因为max变量引用会被释放,fn()函数执行上下文环境也一同被销毁。最后全局上下文环境执行完毕,栈被清空,流程执行结束。 | |
闭包的用途
1. 结果缓存
假如有一个处理很耗时的函数对象,每次调用都会消耗很长时间。我们可以将其处理结果在内存中缓存起来。这样在执行代码时,如果内存中有,则直接返回;如果内存中没有,则调用函数进行计算,更新缓存并返回结果。因为闭包不会释放外部变量的引用,所以能将外部变量值缓存在内存中。
var cachedBox = (function () {// 缓存的容器var cache = {}return {searchBox: function (id) {// 如果在内存中,则直接返回if (id in cache) {return '查找的结果为:' + cache[id]}// 经过一段很耗时的dealFn()函数处理var result = dealFn(id)// 更新缓存的结果cache[id] = result// 返回计算的结果return '查找的结果为:' + result}}})()// 处理很耗时的函数function dealFn(id) {···············console.log('这是一段很耗时的操作')return id}// 两次调用searchBox()函数console.log(cachedBox.searchBox(1)) // 这是一段很耗时的操作 // 查找的结果为:1// 由于第一次已经将结果更新到cache对象中,并且该对象引用并未被回收,直接从缓存中读取// 这样并没有执行很耗时的函数,还间接提高了执行效率。console.log(cachedBox.searchBox(1)) // 查找的结果为:1
2. 封装
在JavaScript中提倡的模块化思想是希望将具有一定特征的属性封装到一起,只需要对外暴露对应的函数,并不关心内部逻辑的实现。
例如,我们可以借助数组实现一个栈,只对外暴露出表示入栈和出栈的push()函数和pop()函数,以及表示栈长度的size()函数。
var stack = (function () {// 使用数组模仿栈的实现var arr = []// 栈return {push: function (value) {arr.push(value)},pop: function () {return arr.pop()},size: function () {return arr.length}}})()stack.push('abc')stack.push('def')console.log(stack.size()) // 2stack.pop()console.log(stack.size()) // 1
优缺点
优点:
- 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
- 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。
缺点:
- 消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是,由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,闭包所存在的最大的一个问题就是消耗内存,如果闭包使用越来越多,内存消耗将越来越大。。
- 泄漏内存:在IE9之前,如果闭包的作用域链中存在DOM对象,则意味着该DOM对象无法被销毁,造成内存泄漏。 ```javascript // 该element元素在网页关闭之前会一直存在于内存之中,不会被释放。 function closure() { var element = document.getElementById(“elementID”) element.onclick = function () { console.log(element.id) } }
// 解决方法: function closure() { var element = document.getElementById(“elementID”); // 使用临时变量存储 var id = element.id; element.onclick = function () { console.log(id); }; // 手动将元素设置为null element = null; } ```





