函数的调用有五种模式:方法调用模式,函数调用模式,构造器调用模式,apply/call调用模式以及回调模式,下面分别对这几种模式进行说明。
1.函数调用与方法调用模式:
1.1 声明一个函数并调用它就是函数调用模式,这是最简单的调用,但其中也关系到this的指向问题。普通函数是将this默认绑定到全局对象,而箭头函数时不绑定this的,在函数所在的父作用域外面this指向哪里,在箭头函数内部this也指向哪里。
function show(name) {
console.log(name);
}
show('shotar'); // shotar
// 普通函数的符符作用域this指向全局作用域,在调用的时候再一次绑定this到全局作用域
function say() {
console.log(this);
}
say(); // 浏览器环境输出window node环境输出global
// 箭头函数父作用域的this指向全局对象,调用的时候并没有绑定this,而是继承父作用域后指向全局对象
var sayName = () => {
console.log(this);
}
sayName() // window
1.2 方法调用时将一个函数作为对象的方法调用,作为方法调用的函数会将this绑定到该对象,但如果方法内部再嵌套一个函数,内部函数再次调用的时候又属于函数调用模式,此时this又将绑定到全局对象。
window.name = 'Jane'; // node环境下是global
var obj = {
name: 'shotar',
sayName: function() {
console.log(1, this.name);
sayWindowName();
function sayWindowName() {
console.log(2, this.name);
}
}
};
obj.sayName(); // 1, shotar 2, Jane
如果想让内部函数(sayWindowName)指向该对象也很简单,在此列举三种方法。第一种是在外部将this保存到一个变量里面,再在内部函数中使用即可。
window.name = 'Jane';
var obj = {
name: 'shotar',
sayName: function() {
var _this = this;
console.log(1, this.name);
sayWindowName();
function sayWindowName() {
console.log(2, _this.name);
}
}
};
obj.sayName(); // 1, shotar 2, shotar
第二种解决办法是使用ES6的箭头函数,箭头函数不绑定this,父作用域的this是哪个对象在箭头函数中的this仍然是哪个对象(注意:箭头函数只能使用函数字面量的形式命名函数名,调用也要在语句之后)。
window.name = 'Jane';
var obj = {
name: 'shotar',
sayName: function() {
console.log(1, this.name);
var sayWindowName = () => {
console.log(2, _this.name);
};
sayWindowName();
}
};
obj.sayName(); // 1, shotar 2, shotar
第三种使用call或apply方法是改变内部函数的this值。
window.name = 'Jane';
var obj = {
name: 'shotar',
sayName: function() {
console.log(1, this.name);
sayWindowName.call(this); // 或 sayWindowName.apply(this);
function sayWindowName() {
console.log(2, this.name);
}
}
};
obj.sayName(); // 1, shotar 2, shotar
在此说明一下阮大大在ES6标准入门里面列举的关于箭头函数this指向的例子,因为在foo函数的作用域下指向window的,使用函数调用模式调用foo函数,setTimeout内的箭头函数不绑定this,还是指向父作用域foo函数所指向的this。foo是普通函数,他将this指向全局对象,因此箭头函数也指向全局变量。这时会打印undefined,为什么又会打印出undefined呢,这是因为在声明id的时候使用了var关键字,他是一个变量并不是全局对象(window或global)的属性,如果将var id = 21;这句改为window.id = 21;(或者global.id = 21)后将打印出21。使用call方法调用会改变this的值,下面到call/apply调用模式的时候会讲到。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo();
foo.call({ id: 42 }); // id: 42
1.3 关于函数this指向问题
普通函数的this是会被绑定的,根据调用方式的不同绑定不同的对象到this(this只能绑定对象),而箭头函数是不绑定this的。有这样一道面试题:
window.bar = 2
var obj = {
bar: 1,
foo: function() {
return this.bar;
}
};
var foo = obj.foo;
console.log(obj.foo()); // 1
console.log(foo()); // 2
JavaScript的this设计很内存里的数据结构有很大的关系。当把一个对象赋给一个变量的时候,大家都知道是引用关系,上面的obj是一个地址,指向那个对象,而在对象存储的时候,其属性(方法)的值也是同样的存储形式,每个属性对应一个属性描述对象,举例来讲,上面obj的bar属性其实是以下面的形式保存起来的。
bar: {
[[value]]: 1,
[[configurable]]: true,
[[enumerable]]: true,
[[writable]]: true
}
其属性的值被保存在[[value]]中。但如果属性的值是个对象(函数也是对象)呢?此时JavaScript引擎会将对象的地址保存在描述符对象的[[value]]位置,像上面的foo属性(方法)则是这样保存的:
foo: {
[[value]]: 对象的地址,
[[configurable]]: true,
[[enumerable]]: true,
[[writable]]: true
}
函数是个单独的值,因此他可以在任何不同的上下文环境中执行,也正因为如此,有必要需要一种机制能够在函数内部获得当前的执行上下文(context),因此this就出现了。在上面的那道面试题中,是将该函数的地址赋给变量foo。通过foo变量调用时,其是在全局作用域下执行,因此this指向全局对象。如图1:
而使用obj.foo执行时,函数是在obj环境下运行,如图2,所以this是指向obj的。上面提到普通函数是绑定this值,this值指得是当前运行环境,当在obj环境下调用时指向obj,而在全局调用时指向全局对象。所以this是在调用时才确定值,并不是在声明时就绑定值。
2.call/apply调用模式
call和apply都是Function.prototype中的方法,可以通过Function.prototype.hasOwnProperty('call')验证。因此每一个函数或者方法都可通过call或apply调用,call和apply都是函数上的方法,每声明一个函数,就像prototype属性一样,都会有call和apply方法。每个函数或方法都可以通过call或者apply改变当前的执行上下文,他们的第一个参数就是要将this绑定的值。区别是后面的传参形式不同,前者是将参数逐个传入调用的函数中,而apply是将参数作为一个数组传给要调用的函数。就拿那道面试题做例子:
window.bar = 2
var obj = {
bar: 1,
foo: function() {
return this.bar;
}
};
var foo = obj.foo;
// ①
foo.call(obj); // 1
// ②
obj.foo.call(window); // 2
// ③
foo.call({bar: 3}) // 3
①如果foo是普通的调用,其this是指向全局对象的,而通过call改变将this绑定到obj后,this将指向obj。我们可以这样理解,foo是这样调用的obj.foo()
②这种调用方式我们可以这样理解,foo是obj的方法,就当他是一个普通的函数,相当于window.foo这样调用,那么this就是指向全局对象的。
③这种调用方式是将{bar: 3}作为this的绑定对象,这样调用foo就相当于{bar: 3}.foo(),this指向{bar: 3}。
3.构造器调用模式:
构造函数的new调用方式被称为构造器调用模式,这是模拟类继承式语言的一种调用方式。在使用new操作符调用函数时,函数内部将this绑定到一个新对象并返回。如下
var Person = function(name) {
this.name = name;
};
var shotar = new Person(shotar);
// 为了区别于普通函数,约定构造函数的首字母大写。使用new操作内部会替你做以下操作:
Person(name) {
// 以下都是使用new操作符时内部做的事
// var obj = new Object();
// this = obj;
// obj.name = name;
// obj.prototype = Person.prototype;
// return obj;
}
如果构造函数内部返回了一个不是对象的值,则new会忽略其返回值而返回新建的对象,如果返回的是一个对象则将其返回。另外,如果不使用new操作符调用,并不会在编译时报错,这是非常糟糕的事情,因此,我们通常会在调用的时候检查是否为new操作符调用,如下:
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
return new Person(name);
}
}
4.回调模式
回调函数是在满足某种情况或者达到某种要求时立即调用。回调函数通常作为函数的参数传入,其本质也还是一种普通的函数,只是在特定的情况下执行而已,先看一个例子:
function sayName(obj) {
var fullName = '';
if (obj.firstName && obj.lastName) {
fullName = typeof obj.computedFullName === 'function' ?
obj.computedFullName() :
obj.lastName + ' ' + obj.firstName;
return fullName;
}
var obj = {
firstName: 'Sanfeng',
lastName: 'Zhang',
computedFullName: function() {
return this.lastName + ' ' + this.firstName;
}
};
sayName(obj); // Zhang Sanfeng
此处的computedName就是一个回调函数,在给sayName函数传值的时候,我们传入了一个对象,前两个属性都是直接在sayName中使用,如果满足这两个属性都有值,那就调用obj的computedName方法(也就是函数),在此处调用就称他为回调函数,回调函数常用于异步操作的场合,比如ajax请求,当请求成功并返回数据时再执行回调函数。一般也用于同步阻塞的场景下,比如执行某些操作后执行回调函数。请先看下面的异步情况的例子:
function ajax(callback) {
var xhr = new XMLHttpReauest();
if (xhr.readystate === 4 && xhr.status === 200) {
typeof callback === 'function' && callback();
} else {
alert('请求失败!')
}
xhr.open('get', url);
xhr.send();
}
var fn = function() {
alert('请求成功!');
};
ajax(fn);
这里会有一个问题,如何给回调函数传参,让回调函数在里面处理一些问题,这里我们就可以用到call或者apply方法了。比如有这样一个问题:统计若干个人的考试成绩,只有90分以上的才发奖学金,请看下面同步阻塞的例子:
function startGive(arr, giveMoney) {
// 先把分数超过90分的过滤出来
let adult = arr.filter(item => item > 90);
// 将过滤结果传入回调函数,发奖金给他们
return giveMoney.call(null, adult);
}
let giveBonuses = function(arr) {
return arr.map(item => item + 'giveMoney');
};
console.log(startGive([70, 80, 92, 96, 85], giveBonuses)); // [ '92giveMoney', '96giveMoney' ]
上面的例子主要是在将分数在90分以上的过滤出来之后再执行操作。回调传参还可以通过传递匿名函数的形式接收该参数,如下例子:
function fn(arg1, arg2, callback){
var num = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
callback(num);
}
fn(10, 20, function(num){
console.log("Callback called! Num: " + num);
});
5.总结
本文讲了关于函数调用的五种模式。五种模式包括函数调用模式、方法调用模式、call/apply调用模式、构造器调用模式和回调模式。其中前三种调用模式类似,主要会涉及到this的指向问题,第四种调用方式总返回一个对象,并将this绑定到此对象。回调模式属于前四种模式中的一种,可以是函数调用模式,也可以是方法调用模式,回调的使用很灵活,其主要场景是用于异步操作或同步阻塞操作的场合。
本文参考《JavaScript语言精粹》一书的函数章节及阮大大的《JavaScript 的 this 原理》一文撰写而出,文中若有表述不妥或是知识点有误之处,欢迎留言指正批评!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。