函数的定义与调用
函数的定义
函数的定义大体上可以分为3种,分别是函数声明、函数表达式和Function构造函数。
1. 函数声明
函数声明是直接使用function关键字接一个函数名,示例如下:
// 函数声明式
function sum(num1, num2) {
return num1 + num2;
}
2. 函数表达式
函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数,示例如下:
// 该函数表达式没有名称,属于匿名函数表达式。匿名函数的this是全局对象window
var sum = function (num1, num2) {
console.log(this) // window
return 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. 求数组中的最大项和最小项
```javascript
var 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. 执行匿名函数
场景:有一个数组,数组中的每个元素是一个对象,对象是由不同的属性构成,现在我们想要调用一个函数,输出每个对象的各个属性值。
```javascript
var 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,会立即执行)
```javascript
function 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 defined
v = '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) // function
foo = 'variable' // 变量赋值
console.log(typeof foo) // string
}
自执行函数
自执行函数即函数定义和函数调用的行为先后连续产生。
function (x) {
alert(x)
} (5) // 抛出异常,Uncaught SyntaxError: Unexpected token (
var aa = function (x) { // 匿名函数表达式
console.log(x)
}(1) // 1
true && function (x) {
console.log(x)
}(2) // 2
0, 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) // 7
new 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) // undefined
function foo() {
console.log(arguments.length) // 3
function foo2() {
console.log(arguments.length) // 0
}
foo2()
}
foo(1, 2, 3)
2. 可通过索引访问**arguments**
对象是一个类数组结构,可以通过索引访问,每一项表示对应传递的实参值,如果该项索引值不存在,则会返回“undefined”。
function sum(num1, num2) {
console.log(arguments[0]) // 3
console.log(arguments[1]) // 4
console.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,表示的是当前正在执行的函数,在比较时是严格相等的。
```javascript
foo()
function foo() {
console.log(arguments.callee === foo) // true
}
通过arguments.callee属性获取到函数对象后,可以直接传递参数重新进行函数的调用,这个属性在匿名的递归函数中非常有用。
function create() {
return function (n) {
if (n <= 1)
return 1
return 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-banana
joinStr(',', '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
模拟函数重载:
```javascript
function sum() {
// 通过call()函数间接调用数组的slice()函数得到函数参数的数组
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
构造函数
构造函数与普通函数区别
- 构造函数的函数名的第一个字母通常会大写。
- 在函数体内部使用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指向的是全局对象
```javascript
var value = 10;
var obj = {
value: 100,
method: function () {
var foo = function () { // 匿名函数表达式,
console.log(this.value); // 10
console.log(this); // Window对象
};
foo();
return this.value;
}
};
- this指向所属对象
console.log(obj.method()); // 100
- this指向对象实例
4. this指向call()函数、apply()函数、bind()函数调用后重新绑定的对象// 全局变量
var number = 10
function 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()函数显得尤为有用:
```javascript
var user = {
data: [
{ name: "kingx1", age: 11 },
{ name: "kingx2", age: 12 }
],
clickHandler: function (event) {
// 随机生成整数0或1
var 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 undefined
button.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 = this
this.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);
// 缓存this
const 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,使其指向实例p
class 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=window
var m = f(1); // window
// 先执行f(2),此时window.m=2,覆盖了window.m=window,最终window.m=2
// 再将返回值“window”赋值给全局变量n,最终window.n=window
var n = f(2); // window
console.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 = 10
console.log(c + x)
}
var bar = function (y) {
var b = 5
fn(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 = 10
return 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()) // 2
stack.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; } ```