函数的调用有五种模式:方法调用模式,函数调用模式,构造器调用模式,apply/call调用模式以及回调模式,下面分别对这几种模式进行说明。node
1.函数调用与方法调用模式:面试
1.1 声明一个函数并调用它就是函数调用模式,这是最简单的调用,但其中也关系到this的指向问题。普通函数是将this默认绑定到全局对象,而箭头函数时不绑定this的,在函数所在的父做用域外面this指向哪里,在箭头函数内部this也指向哪里。ajax
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值。app
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调用模式的时候会讲到。dom
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 原理》一文撰写而出,文中如有表述不妥或是知识点有误之处,欢迎留言指正批评!