函数
- 函数是执行特定任务的代码块
- 函数也是一个对象, 函数名就是个指向函数对象的指针
- 当一个函数是一个对象的属性时,可称之为方法
# 函数定义
# 函数声明
函数声明由一系列function
关键字组成。
function square(number) {
return number * number;
}
// 匿名函数
function (number) {
return number * number;
}
2
3
4
5
6
7
8
# 函数表达式
var square = function (number) {
return number * number;
}
var x = square(4); // 16
2
3
4
# 使用Function
构造函数 (ES6, 不推荐)
var square = new Function ('number', 'return number * number;')
var x = square(4); // 16
2
# 函数提升
在日常开发中,我们经常会混用函数声明和函数表达式来定义函数。实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行 任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
sum(10,10) // 20
function sum(num1, num2){
return num1 + num2;
}
square(10) // square is not a function
var square = function (number) {
return number * number;
}
2
3
4
5
6
7
8
9
10
从上面的例子可以看出,函数声明可以在声明前被调用,函数表达式不可以,这是因为函数声明被提升了。
函数提升有关请看:函数提升
# 函数属性和方法
# 特殊对象
在函数内部,有两个特殊的对象:arguments 和 this。
- arguments(实参对象): 类数组对象,包含着传入函数中的所有参数,可以通过数字下标获得传入实参值。
- this: 函数据以执行的环境对象,可以被改变。
sum(1, 2, 'c', 'd')
function sum(num1, num2){
console.log(num1, num2) // 1 2
console.log(arguments[0]) // 1
console.log(arguments[1]) // 2
console.log(arguments[2]) // c
console.log(arguments[3]) // d
}
2
3
4
5
6
7
8
# 属性
每个函数都包含两个属性:length 和 prototype。
- length:函数希望接收的命名参数的个数
- prototype:函数也是对象,所以它也会有prototype,也会有toString()、valueOf()等方法
- name:返回函数名(ES6)
function sayName(name){
console.log(name);
}
function sum(num1, num2){
return num1 + num2;
}
function sayHi(){
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
console.log(sayName.toString())
console.log(sayName.valueOf())
console.log(sayName.name) // sayName
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方法
每个函数都有以下几个方法:
call(this, param1, param2, ...)
apply(this, arguments)
bind(this, param1, param2, ...)
区别:
- call()、apply()功能一样,只是参数接收方式不同。《call() 和 apply() 的 区别》
- call() 改变 this 指向的时候已经执行,bind() 会生成新一个函数,在调用这个新函数的时候才改变新函数的this
function foo(){
return this;
}
var f1 = foo.call({a:1});
var f2 = foo.apply({a:2});
var f3 = foo.bind({a:1});
console.log(f1); // {a:1}
console.log(f2); // {a:2}
console.log(f3); // function foo(){
// return this;
// }
console.log(foo()); //window对象
console.log(f1()); // f1 is not a function
console.log(f3()); //{a: 1}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apply()和 call() 是非继承的方法,即直接从构造函数中获得。bind()是来自原型链上的方法,即是Function构造函数的原型对象上的一个方法。(这个概念不是很清楚,因为三个方法都能从Function.prototype上获得)
Function.prototype.call()
Function.prototype.apply()
Function.prototype.bind()
2
3
# bind()
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数。 MDN - bind()
应用场景:
- 创建绑定函数,不论怎么调用,这个函数都有同样的 this 值
- 使函数拥有预设的初始参数
- 在 setTimeout 中改变this
- 创建快捷方式
实现一个 bind()思路:
- 参数处理this 和 arguments
- 内部 this 对象用变量保存
- 内部对象使用 apply 改变 this 指向
- 返回一个函数
方法一:不支持使用 new 调用新创建的构造函数
Function.prototype.bind = function(context){
var args = Array.prototype.slice.call(arguments, 1);
var self = this;
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context,finalArgs);
};
};
function foo(){
return this;
}
var f1 = foo.bind({a:1});
f1(); // {a: 1}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方法二:支持使用 new 调用新创建的构造函数
// Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
if (this.prototype) {
// Function.prototype doesn't have a prototype property
// Function.prototype 是没有 prototype 属性的,所以要加上
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# this
在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。
# this 的指向
- 在全局执行环境中(在任何函数体外部),this 都指向全局对象
- 在对象方法中,this 表示该方法所属的对象
- 在函数中,this的值取决于函数被调用的方式。如果没有设置,非严格模式下则指向全局对象,严格模式下为undefined
- 在箭头函数中,this 指向定义时所在的对象,而不是使用时所在的对象
- 在类中,this跟在函数中表现差不多,但在类的构造函数中,this 是一个常规对象,类中所有非静态的方法都会被添加到 this 的原型中
// 在全局执行环境中(在任何函数体外部),this 都指向全局对象
console.log(this === window); // true
// 在对象方法中,this 表示该方法所属的对象
var a = {
name: 'jack',
fun: function() {
console.log(this.name);
}
}
a.fun(); // jack
// 在箭头函数中,this 指向定义时所在的对象,而不是使用时所在的对象
var b = {
name: 'jack',
fun: () => {
console.log(this.name);
}
}
b.fun(); // undefined
// 在函数中,this的值取决于函数被调用的方式
var b = function() {
return this;
}
b() === window; // true
var c = function() {
"use strict";
return this;
}
c() === undefined; // true
// 在类中的表现
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first(){}
second(){}
static third(){}
}
new Example(); // ['constructor', 'first', 'second']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 改变 this 的指向
使用以下方法可以改变 this 指向:
- call()
- apply()
- bind()
具体介绍可看:call() 和 apply() 的 区别、bind()
# 函数参数
- 形参/显式参数(Parameters):函数声明时指定的变量
- 实参/隐式参数(Arguments):函数调用时传入的参数
# arguments对象
函数内部属性,是一个类数组对象,包含着传入函数中的所有参数,可以通过数字下标获得传入实参值。
sum(1, 2, 'c', 'd')
function sum(num1, num2){
console.log(num1, num2) // 1 2
console.log(arguments[0]) // 1
console.log(arguments[1]) // 2
console.log(arguments[2]) // c
console.log(arguments[3]) // d
console.log(Object.prototype.toString.call(arguments)) // [object Arguments]
}
2
3
4
5
6
7
8
9
# ...
拓展符号(ES6)
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
使用注意:
- rest 参数之后不能再有其他参数,否则报错
- 函数的length属性,不包括 rest 参数
function add(...values) {
console.log(Object.prototype.toString.call(values)) // [object Array]
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
2
3
4
5
6
7
8
9
10
11
12
13
# 参数默认值(ES6)
使用注意:
- 不能有同名参数,否则报错
- 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数,即length将失真
sum() // 100
function sum(num1 = 10, num2 = 10){
return num1 * num2
}
2
3
4
默认值不是单纯传值,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo()
2
3
4
5
6
7
8
9
# 函数类型
在函数的介绍中,经常能看到匿名函数、自执行函数、回调函数、高阶函数、闭包等等。有时候对这些函数没有比较清晰的概念,下面是一篇函数类型总结。
# 函数作用域
函数作用域有关请看《作用域》章节
# 闭包
了解闭包之前,需要先了解《作用域》有关知识
# 作用域与闭包
函数的执行依赖于变量作用域,这个作用域是函数定义时决定的,而不是函数调用时决定。函数对象可以通过作用域链相互关联起来,函数体内变量都保存在函数作用域内,这种特性就叫闭包。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
例子1:
function foo() {
var i = 0;
function bar() {
console.log( i++ );
}
bar();
}
foo(); // 0
foo(); // 0
// 执行n次依然输出 0
2
3
4
5
6
7
8
9
10
上面的例子看起来长的很像闭包,但其实它只是基于词法作用域的查找规则,使得函数bar()能够访问外部作用域中的变量i,而每一次foo()执行后都会被销毁,再次调用时 i 被初始化为 0,所以无论执行多少次,输出都是 0。
例子2:
function foo() {
var i = 0;
function bar() {
console.log( i++ );
}
return bar;
}
var baz = foo();
baz(); // 0
baz(); // 1
baz(); // 2
// 执行n次,每次加1
2
3
4
5
6
7
8
9
10
11
12
从例子2可以看出,即使bar()在自己定义的词法作用域外执行,也可以正常执行,因为作用域是定义时就已经决定了的。而 i 能不断加 1 是因为foo()一直没被销毁回收,所以每次执行bar()时,都是在内部作用域中的 i 进行自增。
一般来说,在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为垃圾回收器会释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?就是 bar() 本身在使用。 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。这也就是为什么 i 能够一直不断加1的原因。
# 循环与闭包
在 javascript 中是没有块级作用域的概念的,这就意味着在块语句中定义的变量,实际上是包含在函数中而非语句中创建。
闭包经典面试题
for (var i=1; i<=3; i++) {
console.log(i)
setTimeout( function timer() {
console.log( i );
}, 1000 );
}
// 依次输出 1 2 3 -> 此时循环已经结束,i = 4
// setTimeout 开始执行,间隔 1s,输出 3 个 4
2
3
4
5
6
7
8
显然这个结果不是我们想要的,因为setTimeout的执行在for循环之后,而此时,i已经等于4。
方法一:使用立即执行函数
for (var i=1; i<=3; i++) {
console.log('outer' + i);
(function(j){
console.log('inner' + j) // 内部保存了 i 的值
setTimeout( function timer() {
console.log( j ); // 打印的是内部的变量 j ,而不是外部循环的 i
}, 1000 );
})(i);
}
// 依次输出 outer1 inner1 outer2 inner2 outer31 inner3
// setTimeout 开始执行,间隔 1s, 依次输出 1 2 3
2
3
4
5
6
7
8
9
10
11
方法二:使用 let 来声明变量,let能形成块级作用域
for (let i=1; i<=3; i++) {
console.log(i)
setTimeout( function timer() {
console.log( i ); // 当前循环体内形成一个块级作用域,i被保存,下次循环不影响当前变量
}, 1000 );
}
// 依次输出 1 2 3
// 依次输出 1 2 3
2
3
4
5
6
7
8
方法三:使用bind
for (var i=1; i<=3; i++) {
function timer(i) {
console.log( i );
}
setTimeout( timer.bind(this, i), 1000 );
}
2
3
4
5
6
本质上,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,就会看到闭包在这些函数中的应用。如定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
# 私有变量
在同一作用域内定义多个闭包,它们享有同样的私有变量或变量。
function counter() {
var n = 0;
return {
count: function() {
return n++
},
reset: function() {
n = 0;
}
}
}
var c = counter()
var d = counter()
c.count() // 0
d.count() // 0
c.reset()
c.count() // 0 -> c 已经被reset
d.count() // 1 -> d 没有被影响
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
从上面例子可以看出,c 和 d 虽然都访问counter的私有变量 n,但互相不影响,这是因为每次调用counter() 都会创建一个新的作用域链和一个新的私有变量。
# 模块模式
闭包也是一个非常强大的工具,可以实现多种代码模式,其中最强大的一个就是模块模式(module pattern)。模块模式是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
常见模块模式实现方法
- 模块暴露,暴露内部变量或方法(例子1)
- 在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。(例子2)
例子1
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
例子2
function CoolModule(id) {
// 修改公共 API
function change() {
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = { change: change, identify: identify1 };
return publicAPI;
}
var foo = CoolModule('foo module')
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 现代模块机制与未来模块机制
# 总结
- 闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域
- 当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止
- 闭包可以在对象中创建私有变量
- 闭包可以模仿块级作用域
- 闭包可以实现模块模式
- 过度使用闭包会占用大量内存,影响性能。
# 箭头函数(ES6)
# 优点
- 使表达更加简洁
- 可简化回调函数
- 可跟rest参数
...
结合使用
var f = () => 5;
// 等同于
var f = function () { return 5 };
2
3
更多用法请看 箭头函数 - MDN
# 使用注意
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
- 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
- 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
# 不适用场合
- 定义对象的方法,且该方法内部包括this。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
2
3
4
5
6
- 需要动态this的时候,也不应使用箭头函数。
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
2
3
4
上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
- 函数体很复杂时
如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。