#JS:this的指向及函数调用对this的影响

1,675 阅读10分钟

call、apply、bind和this真是ES5众多坑中的一个,希望本篇文章能让你记住它们!

this的指向

此前在摸清JS中this的指向问题 ,这篇文章中我就尝试总结过this的指向,也顺带提及了call,bind,apply等,本篇文章为了完整性,我再总结一次。

开始之前,请一定要始终把握住关于this的一个概念和原理,心里默念三遍都可以:

this是JS中的关键字,函数运行时自动生成的一个内部对象,只能在函数内部使用。

this永远指向最后调用它的那个对象。

也就是说,this的指向并不是在函数创建的时候确定的,在ES5中,this永远指向最后调用它的那个对象记住这句话,后面很多疑难都一点就通哟。

往深一点说,Javascript是一门文本作用域的语言。也就是说,一个变量的作用域在你写这个变量时就已经确定了。而this关键字的加入是为了在JS中加入动态作用域而做的努力。所谓的动态作用域,也就是变量的作用范围是根据函数调用的位置而确定的。

这么说,你是不是对这句话理解更深一点呢~

详细的栗子请移步摸清JS中this的指向问题 ,这篇文章的1.0开始陈述了各类情况下this的指向

Screen Shot 2018-08-05 at 3.28.54 PM.png

如何改变this的指向

改变this的指向,我总结有以下几种方法:

  • 使用ES6箭头函数
  • 在函数内部使用诸如_this = this
  • 使用apply、call、bind
  • 构造函数环境:用new实例化一个对象
var name = "windowsName";

var a = {
    name : "isaac",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout(  function () {
            this.func1()
        },100);
    }

};

a.func2()     // this.func1 is not a function

这也是一个坑!因为匿名函数的this永远指向window。setTimeout中所执行函数是匿名函数,所以最后调用setTimeout的对象是window,但是在window中并没有func1函数。

我们在改变 this 指向这一节将把这个例子作为 demo 进行改造。

箭头函数

众所周知,ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。箭头函数的 this 始终指向函数定义时的 this,而非执行时。箭头函数需要记着这句话:

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。

var name = "windowsName";

var a = {
    name : "isaac",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout( () => {
            this.func1()
        },100);
    }

};

a.func2() // isaac

_this = this

如果不使用 ES6,那么这种方式应该是最简单的不会出错的方式了,我们是先将调用这个函数的对象保存在变量 _this 中,然后在函数中都使用这个 _this,这样 _this 就不会改变了。

var name = "windowsName";

    var a = {

        name : "isaac",

        func1: function () {
            console.log(this.name)     
        },

        func2: function () {
            var _this = this;
            setTimeout( function() {
                _this.func1()
            },100);
        }

    };

    a.func2() // isaac

在这个例子中,func2中,首先设置var _this = this;,这里的 this是调用func2的对象a,为了防止在func2中的setTimeout中被延迟执行的函数被window调用而导致的在setTimeout中的this为window。我们将 this(指向变量 a) 赋值给一个变量 _this,这样,在 func2 中我们使用_this就是指向对象a了。

使用apply,call,bind

使用call

var a = {
    name : "isaac",

    func1: function () {
        console.log(this.name)
    },

    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.call(a),100);
    }

};

a.func2() // isaac

apply的用法与其一致。

使用bind

var a = {
    name : "isaac",

    func1: function () {
        console.log(this.name)
    },

    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.bind(a)(), 100);
    }

};

a.func2() // isaac

apply、call、bind 区别

刚刚我们已经介绍了 apply、call、bind 都是可以改变 this 的指向的,但是这三个函数稍有不同。

Javascript中关于callapply的用法主要有两个目的

  • 借用构造函数继承
  • 修改函数运行时的this指针

callapply方法的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this获取对它的引用。通俗的说,callapply的作用就在于赋能obj.func.call(thisObj),赋予thisObj这个对象obj.func这个方法(能力)。

注意,在ES5的严格模式中,callapply的第一个实参都会变成this的值,哪怕传入的是null或undefined。

在ES3中和非严格模式下,传入null和undefined都会被全局对象替代,而其他原始值则会被相应的包装对象所替代

而bind需要被手动调用,可以参考mdn的解释

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

call的原理

call的实现原理如下

f.call(o)
// 原理如下
o.m = f // 1. 将f作为o的某个临时属性m来储存
o.m() // 2. 然后执行m
delete o.m // 3. 执行完毕后删除m

举个实例

function fn() {
    var a = 'a'
    this.b = 'b'
    this.sum = function (num1, num2) {return num1 + num2}
}

function o() {
    this.c = 'c'
}

fn.call(o)

此时,o对象内已经包含了fn函数的所有属性和方法。也就是在o中拷贝了一份fn函数的属性与方法,同时修改了this 的指向,即指向新的对象。

栗子

function cat(){}
cat.prototype={     
    food:"fish",     
    say: function(){           
        alert("I love "+this.food);     
    }
}
var blackCat = new cat;
blackCat.say();

如果我们有一个对象whiteDog = {food:"bone"},我们不想对它重新定义say方法,那么我们可以通过call或apply用blackCat的say方法:blackCat.say.call(whiteDog);

所以,可以看出call和apply是为了动态改变this而出现的(改变某个函数运行时的 context 即上下文),当一个object没有某个方法,但是其他的有,我们可以借助call或apply对其它对象进行赋能

用的比较多的,是通过document.getElementsByTagName选择的dom 节点是一种类似类数组对象。它不能应用Array下的push,pop等方法·。我们可以通过:

var domNodes =  Array.prototype.slice.call(document.getElementsByTagName("*"));

这里用slice方法来操作DOMList。因为slice方法返回值就是一个数组本身。 这样domNodes就可以应用Array下的所有方法了。

这里分享一位知乎网友的精辟比喻:

本身不难理解,看下MDN就知道了,但是不常用,遇到了,还要脑回路回转下。或者时间长了,还是要确定下去看下文档,为了方便记忆:猫吃鱼,狗吃肉,奥特曼打小怪兽。有天狗想吃鱼了猫.吃鱼.call(狗,鱼)狗就吃到鱼了猫成精了,想打怪兽奥特曼.打小怪兽.call(猫,小怪兽)就这样记住了。

JS中函数的调用

此前在这篇文章,3.x.2/JS函数的声明、调用及其对this的影响 中,记录过JS中函数调用的几种方式

  1. 函数调用模式
  2. 方法调用模式
  3. 构造器调用模式
  4. call、apply间接调用模式。

函数调用模式

function add(x, y) {
    return x + y
}
var sum = add(3, 4)
console.log(sum)

这是一个最简单的函数,不属于任何一个对象的属性或方法,仅仅是一个函数。

this指向

注意,使用函数调用模式调用函数时,非严格模式下,this会被绑定到全局对象下;严格模式下则是undefined。

function add(x, y) {
    console.log(this) // window
}

另外,因为在函数调用模式的函数中this绑定到全局对象,所以会发生全局属性被重写的情况:

var a = 0
function fn() {
    this.a = 100
}
fn()
console.log(this, this.a, a) //window, 100, 100

因为容易发生命名冲突,所以我们极少这么使用。

方法调用模式

所以说更多的情况是将函数作为对象的方法调用。当一个函数被保存为对象的一个属性时,我们称之为方法。当一个方法被调用时,this被绑定到直接对象上。因此,我们在调用表达式时需要一个提取属性的写法,来调用函数。

var o = {
    m: function() {
        console.log(this === o) // true
        return 1
    }
}
o.m() // 1,这就是调用的方法

这里o对象通过.调用了m方法。然后我们一直记住的那句话this 永远指向最后调用它的那个对象,所以在m中的this就是指向o的。

this指向

方法调用模式下,this指代调用函数本身所属的对象,如上例中的o。因此,通过this我们可以从对象中取值或对对象进行修改。this到对象的绑定发生在调用的时候。通过this可取得它们所属对象的上下文的方法称为公共方法

var o = {
    a: 1
    m: function() {
        return this
    }
    n: function() {
        this.a = 2
    }
}
console.log(o.m().a) // 1
o.n() // 2
console.log(o.n().a) // 2

使用构造函数调用函数

如果函数调用前使用了 new 关键字, 则是调用了构造函数。 这看起来就像创建了新的函数,但实际上JavaScript函数是重新创建的对象:

// 构造函数:
function myFunction(arg1, arg2) {
    this.firstName = arg1;
    this.lastName  = arg2;
}

// This    creates a new object
var a = new myFunction("Li","isaac");
a.lastName;  // 返回 "isaac"

这就有要说另一个面试经典问题:new 的过程了,(ಥ_ಥ) 这里就简单的来看一下 new 的过程吧: 伪代码表示:

var a = new myFunction("Li","isaac");

new myFunction{
    var obj = {};
    obj.__proto__ = myFunction.prototype;
    var result = myFunction.call(obj,"Li","isaac");
    return typeof result === 'obj'? result : obj;
}
  1. 创建一个空对象 obj;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型对象;
  3. 使用 call 改变 this 的指向,指向新建的空对象;
  4. 如果无返回值或者返回一个非对象值,则将 obj返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。

作为函数方法间接调用函数

通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用。

  1. call、apply、bind三者的第一个参数都是this要指向的对象,
  2. bind只是返回函数,还未调用,所以如果要执行还得在后面加个();call、apply是立即执行函数;
  3. 三者后面都可以带参数,call后面的参数用逗号隔开,apply后面的参数以数组的形式传入;bind则可以在指定对象的时候传参,和call一样,以逗号隔开,也可以在执行的时候传参,写到后面的括号中;func.call(obj,value1,value2); func.apply(obj,[value1,value2]); func.bind(obj,value1,value2)(); func.bind(obj)(value1,value2);

在 JavaScript 中, 函数是对象。

JavaScript 函数有它的属性和方法。call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身

在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。


参考资料:

  1. www.cnblogs.com/52fhy/p/511…
  2. www.zhihu.com/question/20…

两篇此前总结的文章

  1. JS函数的声明、调用及其对this的影响
  2. 摸清JS中this的指向问题