02、理解函数调用

616 阅读5分钟

为什么this参数表示函数的上下文?

函数和方法之间有什么区别?

如果一个构造函数显式地返回一个对象会发生什么?

使用隐式函数参数

在函数定义中显式声明的参数之外,函数调用时还会传递两个隐式的参数:arguments和this

隐式的参数在函数声明中没有明确的定义,但会默认传递给函数并且可以在函数中正常访问。在函数内可以想像其他明确定义的参数一样引用它们。

arguments参数

arguments参数是传递给函数的所有参数集合,无论是否有明确定义对应的形参,通过它我们都可以访问到函数的所有参数

举个🌰

function whatever(a, b, c){
  assert(a === 1, 'The value of a is 1');
  assert(b === 2, 'The value of b is 2');
  assert(c === 3, 'The value of c is 3');
​
  assert(arguments.length === 5,
         'We’ve passed in 5 parameters');
​
  assert(arguments[0] === a,
         'The first argument is assigned to a');
  assert(arguments[1] === b,
         'The second argument is assigned to b');
  assert(arguments[2] === c,
         'The third argument is assigned to c');
​
  assert(arguments[3] === 4,
         'We can access the fourth argument');
  assert(arguments[4] === 5,
         'We can access the fifth argument');
}
​
whatever(1,2,3,4,5);
​
function assert(value, text){
  value&&console.log(text)
}
​
// The value of a is 1
// The value of b is 2
// The value of c is 3
// We’ve passed in 5 parameters
// The first argument is assigned to a
// The second argument is assigned to b
// The third argument is assigned to c
// We can access the fourth argument
// We can access the fifth argument

以上代码中whatever函数只定义了三个形参,但在传入的时候传入了5个实参

我们可以通过相应的函数参数a、b、c访问到前三个参数的值

可以通过arguments.length来访问传递给函数的实际参数的个数

可以通过数组下标的方式访问到arguments中的每个参数

注意⚠️:arguments并不是真正的数组,称为类数组,因为并不能尝试使用数组的方法(例如sort)

无参数函数

当定义一个没有显式声明任何参数的函数,也可以通过arguments属性来访问

举个🌰

function sum() {
  var sum = 0;
  for(var i = 0; i < arguments.length; i++){
    sum += arguments[i];
  }
  return sum;
}
​
assert(sum(1, 2) === 3, "We can add two numbers");
assert(sum(1, 2, 3) === 6, "We can add three numbers");
assert(sum(1, 2, 3, 4) === 10, "We can add four numbers");
function assert(value, text){
  value&&console.log(text)
}
// We can add two numbers
// We can add three numbers
// We can add four numbers

以上代码定义了一个没有显式声明参数的sum函数,通过arguments对象访问所有的函数参数,计算它们的和

this参数:函数上下文

当函数调用时,除了显示提供的参数外,this参数也会默认传递给函数。this参数是面向对象JavaScript编程的一个重要组成部分,代表函数调用相关联的对象。通常称为函数上下文

函数调用

函数的调用方式对函数内代码的执行有很大的影响,主要体现在this参数以及函数上下文是如何建立的

首先了解函数的四种调用方式

  • 作为一个函数直接被调用—fun()
  • 作为一个方法,关联在一个对象上,实现面向对象编程—obj.fun()
  • 作为一个构造函数实例化一个新的对象—new Person()
  • 通过函数的apply或者call方法—fun.apply(obj)或者fun.call(obj)

作为函数直接被调用

通过()运算符调用一个函数 ,且被执行的函数表达式不是作为一个对象的属性存在时,就属于直接调用

在这种方式调用时,函数上下文(this)有两种可能:在非严格模式下是全局上下文(window对象),在严格模式下是undefined

举个🌰

function ninja() {
  return this;
}
​
function samurai() {
  "use strict";
  return this;
}
​
assert(ninja() === window,"the context is the global window object");
​
assert(samurai() === undefined,"the context is undefined");
​
function assert(value, text){
  value&&console.log(text)
}
​
// the context is the global window object
// the context is undefined

作为方法被调用

当一个函数被赋值给一个对象的属性,并通过对象属性引用的方式调用时,函数会作为对象的方法被调用。

这种情况下函数被称为方法,在面向对象编程中方法内部的this可以访问到对象的主体,这里也是一样,当一个函数被作为对象的方法调用时,该对象会成为函数的上下文

举个🌰

function whatsMyContext() {
  return this;
}
​
assert(whatsMyContext() === window,"Function call on window");
​
var getMyThis = whatsMyContext;
​
assert(getMyThis() === window,"Another function call in window");
​
var ninja1 = {
  getMyThis: whatsMyContext
};
​
assert(ninja1.getMyThis() === ninja1,"Working with 1st ninja");
​
var ninja2 = {
  getMyThis: whatsMyContext
};
​
assert(ninja2.getMyThis() === ninja2,"Working with 2nd ninja");
​
function assert(value, text){
  value&&console.log(text)
}
​
// Function call on window
// Another function call in window
// Working with 1st ninja
// Working with 2nd ninja

以上代码中

whatsMyContext函数唯一的功能是返回它的函数上下文

  1. 当直接通过函数调用时,函数上下文是全局上下文window
  2. 当通过变量getMyThis引用whatsMyContext函数时,在非严格模式下函数的上下文也是window
  3. 点在一个对象中的属性getMyThis引用whatsMyContext函数时,通过方法引用调用函数时,函数上下文为该对象所在的对象

作为构造函数调用

构造函数的方式调用,需要在函数调用之前使用关键字new

将函数作为构造函数调用是JavaScript中一个强大的特性。使用new关键字调用会触发以下几个动作

  1. 创建一个新的空对象
  2. 该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文
  3. 执行函数中的代码
  4. 新构造的对象作为new运算符的返回值

举个🌰

function Ninja() {
  this.skulk = function() {
    return this;
  };
}
​
var ninja1 = new Ninja();
var ninja2 = new Ninja();
​
assert(ninja1.skulk() === ninja1,"The 1st ninja is skulking");
assert(ninja2.skulk() === ninja2,"The 2nd ninja is skulking");
​
function assert(value, text){
  value&&console.log(text)
}
​
// The 1st ninja is skulking
// The 2nd ninja is skulking

以上代码通过两次调用定义的构造函数,创建了两个新的对象。将返回结果保存在变量中,通过变量引用新创建的对象。

构造函数的返回值

注意⚠️:构造函数通常以大写字母开头

构造函数的目的是初始化新创建的对象,并且新构造的对象会作为构造函数的调用结果返回。但当构造函数自身有返回值时是什么结果呢

举个🌰

function Ninja() {
  this.skulk = function () {
    return true;
  };
​
  return 1;
}
var ninja = new Ninja();
assert(Ninja() === 1,"Return value honored when not called as a constructor");
​
assert(typeof ninja === "object","Object returned when called as a constructor");
​
function assert(value, text){
  value&&console.log(text)
}
​
// Return value honored when not called as a constructor
// Object returned when called as a constructor

以上代码中Ninja函数虽然会毁了数字1,但对代码没有影响。如果Ninja作为一个函数调用,会返回1,但是如果通过new关键字将其作为构造函数调用,构造函数会返回一个新的对象,并不是1

在举个🌰

 var puppet = {
   rules: false
 };
​
function Emperor() {
  this.rules = true;
  return puppet;
}
​
var emperor = new Emperor();
​
assert(emperor === puppet, "The emperor is merely a puppet!");
assert(emperor.rules === false, "The puppet does not know how to rule!");
function assert(value, text){
  value&&console.log(text)
}
​
// The emperor is merely a puppet!
// The puppet does not know how to rule!

上面代码稍有不同,主要是函数中返回了puppet所引用的对象,结果却是截然不同。由此可知:

如果构造函数返回了一个对象,则该对象将作为整个表达式的值返回,而传入的this将被丢弃

但是如果构造函数的返回值是非对象类型,则返回值被忽略,返回新创建的对象

使用apply和call方法调用

在一些特定的场景中我们或许想要改变函数的上下文

举个🌰

function Button(){
  this.clicked = false;
  this.click = function() {
    this.clicked = true;
    assert(button.clicked,"The button has been clicked");
  }
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
function assert(value, text){
  value&&console.log(text)
}
​
//

以上代码,我们定义了一个按钮,检测它是否被单击过,使用构造函数创建一个button的实例化对象,通过该对象存储被单击的状态,当代码执行时会发现失败了,但是却没有报错

其实是浏览器的事件处理系统将把调用的上下文定义为事件触发的目标对象元素了,所有当点击时设置的状态错误的处理了

而通过apply和call方法可以设置函数的上下文

使用apply和call

JavaScript在内置的Function构造函数上保存了这两个方法,可以指定任何对象作为函数的上下文

apple方法调用需要传递两个参数:作为函数上下文的对象、一个数组作为函数调用的参数

call方法与apply使用类似,不同点在于直接以参数列表的形式传递

举个🌰

function juggle() {
  var result = 0;
  for (var n = 0; n < arguments.length; n++) {
    result += arguments[n];
  }
  this.result = result;
}
​
var ninja1 = {};
var ninja2 = {};
​
juggle.apply(ninja1,[1,2,3,4]);
juggle.call(ninja2, 5,6,7,8);
​
assert(ninja1.result === 10, "juggled via apply");
assert(ninja2.result === 26, "juggled via call");
function assert(value, text){
  value&&console.log(text)
}
​
// juggled via apply
// juggled via call

上面的代码执行中设置两个对象作为函数的上下文。当代码执行时result属性被存储在了传入的上下文中

apply和call之间唯一的不同之处在于如何传递参数。apply使用参数数组,call以此列出参数

这两个方法对于指定上下文对象时特别有用,尤其在执行回调函数时

解决函数上下文的问题

箭头函数和bind方法可以更优雅地实现相同的结果

箭头函数

还是按钮点击的🌰

function Button() {
  this.clicked = false;
  this.click = () => {
    this.clicked = true;
    assert(button.clicked, "The button has been clicked");
  }
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
function assert(value, text){
  value&&console.log(text)
}

以上代码中执行正常

调用箭头函数时,不会隐式传入this参数,而是从定义时的函数继承上下文,在上面代码中,箭头函数在构造函数内部,this指向新创建的对象本身,因此this指向了新创建的button对象

箭头函数的注意点⚠️

由于this值是在箭头函数创建时确定的,所以会导致一些看似奇怪的问题

举个🌰

var button = {
  clicked: false,
  click: () => {
    this.clicked = true;
    assert(button.clicked,"The button has been clicked");
    assert(this == window, "In arrow function this == window");
    assert(window.clicked, "clicked is stored in window");
  }
}
​
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
function assert(value, text){
  value&&console.log(text)
}
​
// In arrow function this == window
// clicked is stored in window

执行以上代码会发现,button对象无法跟踪clicked的状态,而之前说箭头函数在创建时确定了this的指向。而此处click箭头函数是作为对象字面量定义的,对象字面量实在全局代码中定义的,因此,箭头函数内部的this值是window

使用bind方法

bind方法创建一个新函数,与原函数的函数体相同,新函数被绑定到指定的对象

举个🌰

var button = {
  clicked: false,
  click: function(){
    this.clicked = true;
    assert(button.clicked,"The button has been clicked");
  }
};
var elem = document.getElementById("test");
elem.addEventListener("click", button.click.bind(button));
​
var boundFunction = button.click.bind(button);
assert(boundFunction !== button.click,"Calling bind creates a completyl new function");
​
// Calling bind creates a completyl new function
// The button has been clicked

以上代码中绑定到按钮点击的事件是通过bind创建的新函数,不管该函数在何时调用,函数内部的this都是通过bind创建时传入的对象

总结

当调用函数时,除了传入的在函数定义中声明的参数之外,同时还传入两个隐式参数:arguments与this

arguments参数时传入函数的所有参数的集合类数组,具有length属性。

this表示函数上下文。函数的定义方式和调用方式决定了this的取值

函数的调用方式有4种

  1. 作为函数调用
  2. 作为方法调用
  3. 作为构造函数调用
  4. 通过call、apply方法调用

函数的调用方式影响this的取值

作为函数调用,非严格模式下this为window,严格模式下this为undefined

作为方法调用,this通常指向调用的对象

作为构造函数调用,this指向新创建的对象

通过call、apply调用,this指向传入的第一个参数

箭头函数没有单独的this,this在箭头函数创建时确定

bing方法用来创建一个新函数,this绑定到传入的参数上。被绑定的函数与原函数具有一致的行为