JS进阶篇-this指向问题

JS中this的指向问题不同于其他语言,JS中的this不是指向定义它的位置,而是在哪里调用它就指向哪里。

JS中,普通的函数调用有三种:直接调用、方法调用和new调用。除此之外还有一些特殊的调用方式,比如通过bind()将函数对象之后在进行调用,通过apply()、call()进行调用等。而es6引入箭头函数之后,箭头函数调用时,其this指向又有所不同,下面来分析这些情况下的this指向。

直接调用:

首先看直接调用,就是通过 函数名(...) 这种方式调用。这时候,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在node中全局对象是 global。

// 简单兼容浏览器和node的全局对象

const _global = typeof window === "undefined" ? global : window;

function demo(){

    console.log(this === _global);    // true

}

demo();    // 直接调用

注意⚠️:直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用。比如下面这个例子也是直接调用:

(function(_global){

    // 通过IIFE 限定作用域

    function test(){

        console.log(this === _global);    // true

    }

    test();    // 非全局作用域下的直接调用

})(typeof window === "undefined" ? global : window);

bind()对直接调用的影响:

这种情况在react和es6中经常遇到,绑定事件其实类似于之前的 var _this = this;

Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其this始终指向绑定的对象。例:

const obj = {};

function demo(){

    console.log(this === obj);

}

const obj1 = demo.bind(obj);

test();    // false

obj1();    // true

那么 bind 做了什么:

const obj = {};

function demo(){

    console.log(this === obj);

}

function myBind(func, target){

    return function(){

        return func.apply(target, arguments);

    }

}

const obj1 = myBind(demo, obj);

test();    // false

obj1();    // true

从上面的示例可以看到,首先通过闭包保持了target,即绑定的对象;然后在调用函数的时候,对原函数使用了 apply 方法来指定函数的 this。当原声的 bind() 实现可能不同,而且更高效。但这个示例说明了 bind() 的可行性。

call和apply对this的影响:

上面用到了 Function.prototype.apple(),与之类似的还有 Function.prototype.call()。他们第一个参数都是指定函数运行时其中的 this 指向。不过使用 apply 和 call 的时候仍需要注意,如果目标函数本身是一个绑定了 this 对象的函数,那么 apply 和 call 不会像预期那样执行。比如:

const obj = {};

function demo(){

    console.log(this === obj);

}

// 绑定到一个新对象,而不是obj

const obj1 = demo.bind({});

demo.apply(obj);    // true

obj1.apply(obj);    // false

这样看来 bind 对函数的影响还是很大的,所以使用时一定要注意!

方法调用:

方法调用是指通过对象来调用其方法函数,它是 对象.函数名(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是同样需要注意 bind() 的影响。

const obj = {

    // 第一种方式,定义对象的时候定义其方法

    test() {

        console.log(this === obj)

    }

}

// 第二种方式,对象定义好之后为期附加一个方法(函数表达式)

obj.test2 = function(){

    console.log(this === obj)

}

// 第三种方式与第二种原理相同

// 是对象定义好之后为其附加一个方法(函数定义)

function t(){

    console.log(this === obj)

}

obj.test3 = t;

// 这也是为对象附加一个方法函数

// 但这个函数绑定了一个不是 obj 的其他对象

obj.test4 = (function(){

    console.log(this === obj);

}).bind({})

obj.test();    // true

obj.test2();    // true

obj.test3();    // true

obj.test4();    // false

这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj 对象作为其方法。再次强调,函数内部的 this 指向与定义无关,受调用方式的影响。

方法中 this 指向全局对象的情况:

注意,这里说的方法中不是方法调用中。方法中的 this 指向全局对象,如果不是因为 bind(),那一定是因为不是用的方法调用方式,比如:

const obj = {

    test(){

        console.log(this === obj);

    }

}

const t = obj.test;

t();     // false

t就是obj的test方法,但是t()调用时,其中的this指向全局。

之所以要提出这种情况,主要是因为常常将一个对象方法作为回调传给某个函数之后,却发现运行结果与预期不符—因为忽略了调用方式对 this 的影响。比如下面的例子就是在页面中对某些事情进行封装之后特别容易遇到的问题:

class Handles {

    // 这里 $button 假设是一个指向某个按钮的jq对象

    constructor(data, $button) {

        this.data = data;

        $button.on('click', this.onButtonClick);

    }

    onButtonClick(e) {

        console.log(this.data);

    }

}

const handles = new Handles("string data", $('#btn'));

// 对 $('#btn') 进行点击操作之后,输出 undefined ,但预期输出 string data

很显然 this.obButtonClick 作为一个参数传入 on() 之后,事件触发时,是对这个函数进行的直接调用,而不是方法调用。所以其中的 this 指向的是全局对象,要解决这个问题有很多办法:

// 这是es5 中的解决办法之一

var _this = this;

$button.on('click', function(){

    _this.onButtonClick();

})

// 也可以通过 bind() 来解决

$button.on('click', this.onButtonClick.bind(this));

// es6 中可以通过箭头函数来处理,在jq中慎用

$button.on('click', e => this.onButtonClick(e));

不过请注意,将箭头函数用作jq的回调时,要小心函数对 this 的使用。jq大多数回调函数【非箭头函数】中的 this 都是表示调用目标,所以可以写 $(this).text() 这样的语句,但jq无法改变箭头函数的 this 指向,同样的语句语义完全不同。

new调用:

在es6之前,每一个函数都可以当作构造函数,通过 new 调用来产生新的对象(函数内无特定返回值的情况下)。而es6改变了这种状态,虽然 class 定义的类用 typeof 运算符得到的仍然是“function”,但他不能像普通函数一样直击调用;同时,class 中定义的方法函数,也不能当作构造函数 new 来调用。

而在es5中, 用 new 调用一个构造函数,会创建一个新对象而其中的 this 指向这个新对象。这没什么悬念,因为 new 本身就是设计来创建先对象的。

var data = 'hi';

function AClass(data){

    this.data = data;

}

var a = new AClass('hello world');

console.log(a.data);    // hello world    this === a 

console.log(data);    // hi

var b = new AClass('hello world');

console.log(a === b);    // false

箭头函数中的this:

箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含他的那个函数或函数表达式中的 this。比如:

const obj = {

    test(){

        const arrow = () => {

            // 这里的 this 是 test() 中的 this,

            // 由 test() 的调用方式决定

            console.log(this === obj)

        }

        arrow();

    },

    getArrow() {

        return () => {

            //  这里的 this 是 getArrow() 中的 this

            // 由 getArrow() 的调用方式决定

            console.log(this === obj);

        }

    }

}

obj.test();    // true

const arrow = obj.getArrow();

arrow();    // true

示例中两个 this 都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this 是由其调用方式决定的。上例的调用方式都是方法调用,所以 this 都指向方法调用的对象,即 obj。

箭头函数让大家在使用闭包时不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用。来看一段 Babel 对箭头函数的转译可能能加深理解:

// es6

const obj = {

    getArrow(){

        return () => {

            console.log(this === obj)

        }

    }

}

// es6 babel 转译

var obj = {

    getArrow: function(){

        var _this = this;

        return function(){

            console.log(_this === obj);

        }

    }

}

另外需要注意的是,箭头函数不能用 new 调用,不能 bind() 到某个对象(虽然 bind() 方法调用没问题,但不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含他的最近的一层函数或函数表达式)绑定的 this。

总结: [以下方法均需要注意 bind() 的影响]

直接调用[任何作用域]:函数名(...) => 函数内部 this 指向全局对象。typeof window === 'undefined' ? global : window

bind():Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,返回一个新函数,不论此新函数什么方式调用,其 this 始终指向绑定的对象,相当于=>[var _this = this;]

call()/apply():Fcuntion.prototype.call()/Function.prototype.apply()。他们的第一个参数都是指定函数运行时其中的 this 指向。

方法调用:对象.方法函数(...) => 函数内部 this 指向该方法的对象。

new调用:使用new一个构造函数会创建一个新对象,而 this 指向这个新对象。

箭头函数:没有自己的 this 绑定,箭头函数中使用的 this,其实是直接包含他的那个函数或函数表达式中的 this。

obj.demo() / var d = obj.demo();       // 均为 true

方法中 this 指向全局对象的情况:bind() / const d = obj.demo();

关于bind、call和apply之间区别的文章请看这里:bind、call和apply之间区别

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容