重温JavaScript(lesson7):函数(3)

在lesson6中我们回顾了有关函数参数的内容,本次我们将一同研究函数调用的相关知识。当函数调用时,除了显式的提供参数外,this指针参数也会默认地传递给函数。再说一次,当函数调用时,this指针参数也会默认地传递给函数。这一次我们的重点就是this。

1.函数this指针参数是什么?

this代表函数调用相关联的对象,也就是代表调用函数的对象,也叫做函数上下文。在全局作用域中,this代表全局对象(浏览器里的window)。当一个函数作为对象的方法被调用时,默认this的值等于那个对象。看代码:

var person = {
  name: "New_Name",
  sayName: function() {
    console.log(person.name);
  }
}
person.sayName();//New_Name

定义了对象person,它拥有属性name和sayName,sayName是一个函数。但是sayName有一个问题,它直接引用了person.name,如果你想给person换个变量名,那么sayName函数里面的person也得改成别的变量,这就造成了方法和对象间的紧耦合。我们可以将sayName中的person改成this,代码如下:

var person = {
  name: "New_Name",
  sayName: function() {
    console.log(this.name);
  }
}
person.sayName();//New_Name

这次,sayName引用的是this而不是person。在调用的时候,是person调用sayName,而this代表调用函数的对象,此时就代表person,所以输出person的name值。再看一段代码:

function sayNameCommon() {
  console.log(this.name);
}
var person = {
  name: 'New_Name',
  sayName: sayNameCommon
}
var officialAccount = {
  name: '重温新知',
  sayName: sayNameCommon
}
var name = 'JavaScript';
person.sayName();
//New_Name
officialAccount.sayName();
//重温新知
sayNameCommon();
//严格模式this指向undefined 报错
//非严格模式 打印 JavaScript

本例定义了函数sayNameCommon,然后创建了两个对象字面量person和officialAccount。之后它们分别调用了

sayNameCommon输出了自己的name。之所以它们能分别输出自己的name,是因为this在函数调用时才被设置。

最后声明了全局变量name,全局变量被认为是全局对象的属性,所以直接调用sayNameCommon的时候相当于使用window调用它。

函数的调用方式对函数内代码的执行有很大的影响,主要体现在this参数和函数上下文是如何建立的。我们要来看看函数调用的4种方式。

2.函数调用的4种方式

我们可以通过以下4种方式调用一个函数,它们之间有一些差别。

第一,作为一个函数被调用;

第二,作为一个方法,关联在一个对象上,被对象调用;

第三,作为一个构造函数,实例化一个新的对象;

第四,使用apply或者call方法调用,可以改变this。

我们先来简单地看一下这4种调用方式:

function sayName(name) {
  console.log(name);
}

sayName('New_Name');
//New_Name 作为函数直接被调用

var person = {
  name: 'New_Name',
  sayMyName: function(){
    console.log(this.name)
  }
}
person.sayMyName();
//New_Name 作为对象的一个方法被调用

function Person(name){};
var handsome = new Person('New_Name');
console.log(handsome)
//Person {} 作为构造函数调用

sayName.call(person,'New_Name');
//New_Name 通过call调用
sayName.apply(person,['New_Name']);
//New_Name 通过apply调用

除了call和apply的方式外,函数调用的操作符都是函数表达式之后加一圆括号。下面我们逐个地详细探讨这几种调用方式。

2.1函数作为一个函数被调用

“函数作为一个函数调用”是不是听着有些别扭呢?函数肯定要作为函数调用啊,但是这么说是为了区别于其他形式的调用方式:作为对象的方法、作为构造函数和apply/call。如果不是通过这三种方式调用一个函数,我们就称之为作为函数直接调用。我们来看例子:

function sayName(name) {
  console.log(name);
}
sayName("hello");
//hello 这是函数定义作为函数被调用

var newName = function() {
  console.log('New_Name');
}
newName();
//New_Name 这是函数表达式作为函数被调用

(function(){
  console.log("Stranger");
})();
//Stranger IIFE 立即调用的函数表达式作为函数被调用

当以此种方式调用一个函数时,函数上下文即this指针参数有两种可能:在非严格模式下,它将是全局上下文(window对象);而在严格模式下,它将是undefined。

2.2 作为方法被调用

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

var person = {};
person.sayName = function() {
  console.log('New_Name');
}
person.sayName();
//New_Name

这种情况就属于函数作为对象方法被调用。当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。再来看一个例子:

function getContext() {
  return this;
}
var person1 = {
  name:"p1",
  getThis: getContext
}
var person2 = {
  name:"p2",
  getThis: getContext
}
console.log(person1.getThis());
//{name: "p1", getThis: ƒ}
console.log(person2.getThis());
//{name: "p2", getThis: ƒ}

通过本例,我们看到虽然person1和person2共享了完全相同的函数,但当执行函数时,该函数的this指针指向的是此时调用函数的对象,也就是函数上下文。我们不需要创建单独的函数副本来操作不同的对象却进行相同的操作,这就做到了复用。

下面我们来看和函数调用相关的另外一种方式:构造函数。

2.3作为构造函数调用

先看一段代码:

function Person(name){
  this.name = name;
  this.sayName = function(){
    console.log(this.name)
  };
  this.whoAmI = function(){
    return this;
  }
}
var p1 = new Person("p1");
var p2 = new Person("p2");
p1.sayName();
//p1
p2.sayName();
//p2
console.log(p1 === p1.whoAmI());
//true
console.log(p2 === p2.whoAmI());
//true

在本例中,我们创建了一个叫Person的函数作为构造函数。通过new关键字调用时会创建一个空的对象实例,并将其作为函数的上下文(this指针参数)传递给函数。构造函数中在该对象上创建了一个名为name的属性并赋值为参数name的值;创建一个名为whoAmI的属性并赋值为一个函数,使得该函数称为新创建对象的一个方法。

要注意,当函数作为构造函数被调用的时候会发生一系列操作。使用关键字new调用函数会触发以下几个动作:

第一,新创建一个空对象;

第二,该对象作为this参数传递给构造函数,从而作为构造函数的函数上下文;

第三,新创建的对象作为new运算符的返回值。

下面我们看一下使用call/apply来调用函数。

2.4使用call和apply方法调用

相信通过以上的内容,你已经看出来了:不同类型的函数调用之间的主要区别在于被调用函数的函数上下文不同。对于方法而言,上下文是方法所在的对象;对于构造函数,上下文是新创建的对象实例;对于顶级函数而言是window或者undefined。函数会在各种不同的上下文中被使用,它们必须八面玲珑!

一般而言,this会自动设置,但是如果想改变上下文,也就是说要改变this的指向该怎么办呢?我们可以使用call方法或者bind方法。但是你可能要问:我们什么情况下需要改变this指向呢?看一个例子:

function Button() {
    this.clicked = false;
    this.click = function() {
      this.clicked = true;
      console.log(button.clicked);
    }
  }
  var button = new Button();
  //在html中已经定义了id为btn的按钮
  var btn = document.getElementById("btn");
  btn.addEventListener("click",button.click);

当我们点击页面中的按钮时候,输出仍然是false。为什么呢?如果通过button.click()调用函数,上下文将是按钮,因为函数将作为button对象的方法被调用。但是,浏览器事件处理系统将把函数的调用定义为事件触发的目标元素,因此上下文将是button元素而不是button对象。也就是函数的上下文和我们预期的不一样。下面我们就来看看改变函数上下文的方法:

2.4.1call()方法

我们首先看看call方法,call的第一个参数指定了函数执行时的上下文,也就是函数执行时的this值,剩下所有的参数都是要被传入函数的参数。

function sayName(scope) {
  console.log(scope,this.name);
}
var person1 = {
  name: "New_Name"
}
var person2 = {
  name: "重温新知"
}
sayName.call(person1,"person1");
//person1 New_Name
sayName.call(person2,"person2");
//person2 重温新知

如上代码中,sayName接受参数scope,在函数体中打印scope和当前上下文中的this值。两次调用分别使用person1和person2,由于使用call方法我们就显示地指定了this的值而不是让JavaScript引擎自动指定。

2.4.2 apply()方法

apply是用来操作this指针的第二个方法,其工作方式和call相同。但是它就接受两个参数,一个函数调用的上下文也就是this的值,另外一个是参数组成的数组。我们不需要像使用call那样一个一个的指定参数,而是通过一个数组把参数整个地传过去。看代码:

function sayName(scope) {
  console.log(scope,this.name);
}
var person1 = {
  name: "New_Name"
}
var person2 = {
  name: "重温新知"
}
sayName.apply(person1,["person1"]);
//person1 New_Name
sayName.apply(person2,["person2"]);
//person2 重温新知

在上述代码中,使用了apply,其结果和call完全相同。那么问题来:什么时候用call,什么时候用apply?答案是:

如果你方便准备一个数组作为参数,那么就用apply。如果你只有单独的一个一个变量,则用call。

好,现在我们知道了call和apply可以改变this指针的指向。那么像Button的那个例子是不是就有解决方案了啊?

我们尝试把代码中的关键一句改为如下形式:

btn.addEventListener("click",button.click.call(button));

这会导致还没点击按钮就直接输出一个true,这显然不是我们想要的结果,说明这种情况下用call还有点不合适(为啥就不行,原理上New_Name还不是很清楚,明白的大佬留个言)。那我们有别的方法吗?有!第三个改变this的函数叫做bind()

2.4.3 bind()方法

bind方法有别于call和apply。bind()的第一个参数是要传给新函数的this值。其他所有参数代表需要永久设置在新函数中的命名参数。可以在之后继续设置任何非永久参数。看代码:

function sayName(scope) {
  console.log(scope,this.name);
}
var person1 = {
  name: "New_Name"
}
var person2 = {
  name: "重温新知"
}
var sayName1 = sayName.bind(person1);
sayName1("person1");
//person1 New_Name
var sayName2 = sayName.bind(person2,"person2");
sayName2();
//person2 重温新知
person2.sayName = sayName1;
person2.sayName("person2");
//person2 New_Name

在如上代码中sayName1只是绑定了this参数,但是没有绑定参数,所以调用的时候传参了。sayName2绑定了this参数也绑定了参数。最后是将sayName1设置为person2的sayName方法,由于sayName1的this参数已经绑定了,所以输出来的this.name是person1的name,其值为New_Name。

我们用bind来尝试解决Button的例子:

btn.addEventListener("click",button.click.bind(button));

代码的运行效果是:点击按钮之后输出true。这是符合我们期望的结果的。这是因为无论什么时候点击按钮,都将调用绑定的函数,函数的上下文都是button对象。这个问题终于解决了吧。其实我们还有一种方法来处理Button示例中的问题,那就是使用箭头函数。

2.4.4 使用箭头函数绕过函数上下文

function Button() {
    this.clicked = false;
    this.click = () => {
      this.clicked = true;
      console.log(button.clicked);
    }
  }
  var button = new Button();
  //在html中已经定义了id为btn的按钮
  var btn = document.getElementById("btn");
  btn.addEventListener("click",button.click);

代码的运行效果也是:点击按钮之后输出true。为什么呢?回顾我们之前学到有关箭头函数的内容:箭头函数自身没有上下文,从定义时所在的函数继承上下文。本例中箭头函数定义在Button构造函数中,所以this指向新创建的button对象。

到这里,我们通过3个lesson的时间把函数的函数的定义、函数的参数和函数的调用等相关知识都回顾了一遍,这些就是函数有关的比较重要的基础知识啦。这部分的相关面试题我们会专门找一个时间列出来。当然关于函数,我们还有很多地方没有提及到,比如说函数式编程。以后我们将专门一起学习函数式编程的内容。我们将一起学习JS面向对象的内容,从ES5的古老玩法到ES6的现代玩法,我们都会有所接触,你做好准备了吗?

如有错误,请不吝指正。温故而知新,欢迎和我一起重温旧知识,攀登新台阶~

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重温新知

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值