JavaScript 中,我们可以使用四种方式调用函数。

  • 作为普通函数调用
  • 作为对象的方法调用
  • 作为构造器调用
  • 使用 apply() 和 call() 方法调用

在研究四种调用方式之前,我们先来看看函数参数的特殊之处。

函数参数

首先,我们来熟悉下实参形参的概念。

var array = [1, 2];
function fn(a, b) {}
fn(1, array);

上面的代码片段中,function fn(a, b) {} 中的 ab 是函数声明时定义的参数名,称之为形参

fn(1, array); 中的 1array 是在函数调用时实际传递的参数,称之为实参

大多数语言,一般会要求实参与形参必须在数量上匹配,静态类型语言还要求类型匹配。

但在 JavaScript 中,实参跟形参的数量可以不匹配,这是一个很有意思的特性,之后我们再讨论该特性的应用。

JavaScript 分两种策略来处理实参与形参不一致的问题。

  • 实参数量大于形参:不报错,并提供访问多余实参的方法。
  • 实参数量小于形参:不报错,未提供参数的形参值为 undefinded

实际上,当调用函数时,会隐式传递两个参数:argumentsthis

隐式表明这两个参数不需要出现在函数签名中,但可以在函数体内显式访问。

arguments 参数

arguments 是一个类数组结构的对象,储存了所有传递给该函数的显式实参。

(function fn() {console.log(arguments)})(1, 2, 3, 4);
// <- { 0: 1, 1: 2, 2: 3, 3: 4, length: 4, callee: fucntion, ... }

this 参数

this 参数比 arguments 参数更有趣也更重要。this 引用了一个与当前函数调用有关的隐式对象,这个对象称为函数上下文 (function context)。

函数上下文的概念出自于面对对象编程范式,在面对对象中,函数声明在一个类中,而声明在类中的函数又被称作为方法,要调用方法,就可能通过一个类的实例来调用。而 this 就是指这个调用方法的实例。

但在 JavaScript 中,情况发生了转变,因为 JavaScript 是一个多编程范式的编程语言,它不仅包含了面对对象编程范式,还包含了函数式编程范式。所以,在 JavaScript 中,函数不仅仅是作为方法来调用,还存在其他的调用方式。此时,我们将 this 称为调用上下文 (invocation context) 也许更合适。

作为普通函数调用

所谓的普通函数,只是除了通过之后要介绍的三种特殊调用方式之外的所有函数的统称。

// 声明函数
function foo() {}
// 通过 () 操作符调用
foo();
// 声明后立即调用
(function fn() {})()

需要注意的一点是 (function fn() {})() 其实是一个组合表达式。

我们可以通过逐步分解来理解该表达式:

  1. function fn() {} 是一个函数字面量。
  2. (function fn() {}) 是一个表达式,返回一个函数对象。
  3. (function fn() {})() 是个函数调用表达式,通过 () 操作符调用该函数。

我们再来看看,普通函数调用时 this 的值。

(function () { return this })() // window
(function () { return function () { return this } })()() // window

在普通函数中,this 指向全局对象 window

// 启用严格模式
'use strict'
(function () { return this })() // undefined
(function () { return function () { return this } })()() // undefined
function fn() {
return this;
}
fn(); // undefined
window.fn(); // window

严格模式下,普通函数调用的隐式参数 thisundefined

注意最后一条语句,通过 window 对象调用 fn() ,此时 fn 作为 window 对象的方法被调用,thiswindow

作为对象的方法调用

当函数赋值给对象的属性时,我们只能通过 . 操作符调用该函数。

var obj = {
fn: function () {}
};
obj.fn();
fn(); // 报错

此时,我们将这种与对象绑定在一起的函数称之为方法

function fn() {
return this;
}
var obj_1 = {
fn: fn
};
var obj_2 = {
};
window.fn(); // window
obj_1.fn(); // obj_1
obj_2.fn(); // TypeError: obj_2.fn is not a function

方法中的 this 为调用该方法的对象。

作为构造器来调用

当我们在普通函数调用前加上 new 关键字来创建一个对象时,我们一般称该函数为构造器

// 该函数显式返回一个对象
function init () {
return {a: 1};
}
// 普通函数调用
var obj = init();
// 该函数未显式返回一个对象
function Obj () {
this.a = 1;
}
// 构造器调用
var obj2 = new Obj();

使用构造器语法创建一个对象,会执行一些隐式语句,下面给出一个大概的示例:

function Obj () {
<!-- 隐式语句 -->
var obj = {};
obj.__proto__ = Obj.prototype;
this = obj;
<!-- 显式语句 -->
this.a = 1;
<!-- 隐式语句 -->
return obj;
}

__proto__ 是对象的隐式属性,指向原型链的上级。
prototype 是函数的隐式属性,指向函数原型,相当于面对对象中的类。
this 参数为隐式创建的对象。

另一个注意点,new 操作符会默认返回一个对象,但如果用户指定返回对象的话,将会覆盖默认返回的对象。然而,如果用户返回的是原始值的话,最终返回的仍是默认对象。

function P() { this.a = 1; }
new P(); // { a: 1 }
// 返回原始值
function P() { this.a = 1; return 1; }
new P(); // { a: 1 }
function P() { this.a = 1; return 'str'; }
new P(); // { a: 1 }
function P() { this.a = 1; return true; }
new P(); // { a: 1 }
function P() { this.a = 1; return null; }
new P(); // { a: 1 }
function P() { this.a = 1; return undefined; }
new P(); // { a: 1 }
// 返回对象
function P() { this.a = 1; return { a: 2 }; }
new P(); // { a: 2 }
function P() { this.a = 1; return [1]; }
new P(); // [1]
function P() { this.a = 1; return function () {}; }
new P(); // function () {}

使用构造器时,使用新对象覆盖返回对象是没有实际意义,应避免这种用法。

使用 apply()call() 方法调用

有时,我们会想在调用函数时修改默认的 this 参数,JavaScript 提供两个方法可以实现对 this 参数的修改。

applycall 都是函数对象特有的方法。

两者唯一的不同在于接收的参数列表:

// apply(this, [param, param])
String.prototype.indexOf.apply('abcd', ['b']); // 1
// call(this, param, param)
String.prototype.indexOf.call('abcd', 'b'); // 1

这两个方法,接收的第一个参数都将传递给 this 参数。
apply 的第二个参数必须是类数组结构的对象,该对象中的每一个数组元素都将传递给原函数的参数列表;call 则会将第二个参数及其之后的所有参数都传递给原函数的参数列表。

function args () { return arguments; }
args.apply(null, ['a', 'b'], ['c'], 123);
// <- ["a", "b"]
args.call(undefined, 1, 2, 3);
// <- [1, 2, 3]

在回调函数中,我们常常会修改回调函数的 this 参数。

比如,我们可以构建一个 forEach 函数,用来遍历数组,并对每个元素执行一些操作。

function forEach(list, callback) {
if (!list instanceof Array) return;
for (var n = 0; n < list.length; n++) {
callback.call(list[n], n);
}
}
forEach([1, 2, 3], function (element, index) {
console.log(index + ' : ' + element);
}
// <-
// 0 : 1
// 1 : 2
// 2 : 3

this 深入

关于 this 参数,还有几点需要深入讨论一下。

this 不会被继承

var obj = {
fn: function () {
console.log(this); // 01. obj
setTimeout(function () {
console.log(this); // 02. Window
}, 1);
(function () { console.log(this) })(); // 03. Window
}
}
obj.fn();
  1. obj.fn() 通过对象 obj 调用 fn,所以 fnthis 参数为 obj
  2. setTimeout 的回调函数是一个普通函数调用,所以 this 参数为 window
  3. 内嵌函数也是作为普通函数调用,所以 this 参数为 window

内部函数的 this 参数与外部函数的 this 没有关联,即 JavaScript 默认不会将 外部函数的 this 参数传递给内部函数的 this 参数。
要想内部函数访问外部函数的 this 参数,就必须先在外部函数保存 this 参数到变量中(假设是 self),再在内部函数中访问 self即可。

在 DOM 事件的事件处理器中 this 为事件触发对象

<button id="btn">click</button>
<script>
var btn = document.querySelector('#btn');
btn.onclick = function () {
console.log(this); // HTMLButtonElement
}
// 模拟按钮点击,触发点击事件
btn.click();
</script>