this的正确用法

何为Call-Site

表示的是:函数在哪里被调用的。

一切都是规则

四条军规。

军规一:默认绑定

其他规则不匹配时就用这个规则啦。

第一件值得注意的事情是:全局环境下声明的变量,就是全局对象的属性了。

function foo() {
    console.log( this.a );
}
var a = 2; // 自动变成全局对象的属性

foo(); // 2

这里的this绑定就指向了全局对象,并拿到了全局对象的属性a.

但是,如果开启了strict模式,则全局变量就不适用于默认绑定了,this会变成未定义。

总结:总体来说,this绑定规则完全是根据Call Site的,全局对象只在非严格模式下才能用于默认绑定。严格模式下,会判定foo函数的调用为无关。

另外注意一点是,你的程序要么是严格模式,要么是非严格模式,不可以一边严格一边不严格。有时候引入的第三方库和你的代码里的模式会有不同。

隐式绑定

上下文对象。

拥有或者包含其他对象的对象。

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

这里的obj就是上下文对象。

foo函数用作obj对象的reference property

这里的this就指向obj对象。

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
  a: 2,
  obj2: obj2
};
obj1.obj2.foo(); // 42

this认的是最近的这个。

隐式丢失

这是很常见的一类错误。

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = bar.foo; // 函数别名,函数的reference
var a = "oops, global";
bar(); // "oops, global"

这里如果是我自己想的话,会觉得输出为2.

之所以又跑到全局的原因是:barrefer到的是foo本身,不带有obj这个上下文对象。

还有更隐蔽更让人摸不到头脑的呢!

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    fn(); // fn只会被认为是另一个foo函数的reference
}

var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";
doFoo(obj.foo); // "oops, global"

这个是真的难以察觉的,因为会认为fn()Call Site,但实际上fn是函数foo的别名。

即使是内建函数调用也是一样的效果:

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    fn(); // fn只会被认为是另一个foo函数的reference
}

var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";
setTimeout(obj.foo, 100); // "oops, global"

结果是一样的。

如何避免这些问题呢?

显式绑定

在JS中的所有函数都有一些特性可用,通过原型链

而且,函数有call()apply()方法可用。

看代码就明白了:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2

通过foo.call(obj)可以强行把this绑定到obj上。

来来来,我喊到的谁谁就是当前的this啦!

硬绑定

functionn foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};
var bar = function() {
    foo.call( obj ); // 强制性把`this`绑定给obj
};
bar(); //2
setTimeout( bar, 100 ); //2
bar.call(window); // 2

bar函数内部强制绑定。

最典型的硬绑定的方式是下面这种,可以传递任意参数的:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = function() {
    return foo.apply( obj, arguments ); 
}
var b = bar(3); // 2 3
console.log( b ); // 5

这种写法不是特别能理解,bar函数明明不接受参数,只是在return返回的是foo.apply( obj, arguments)。但是调用bar函数时传递了参数!?

为了表示这种模式,可以用下面的更加可以复用的写法:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
// 将函数fn和obj绑定在一起
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
}
var obj = {
    a: 2
};
var bar = bind( foo, obj);
var b = bar( 3 ); // 2 3
console.log( b ); // 5

整体输出效果和上面的案例是一样的。只不过是用bind这种写法会更加可复用。

既然硬绑定这么普遍,所以这在ES5中是默认提供的了:

Function.prototype.bind

具体使用方式如下:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind( obj ); // 直接就bind了foo函数内的this给obj了
var b = bar( 3 ); // 

语言层面实现好,真的大大减轻工作量的!一行就硬绑定,那么我也理解为什么会有函数绑定的用法了~一切都源于`this`这个任性的关键词啊

API内叫作contexts

很多JS内置函数和主机环境提供一个可选关键词,称作context,设计用来避免使用bind却能保证回调函数正确使用了this关键词。

下面的例子一下没看明白,看了两次才看懂:

function foo(el) {
    console.log( el, this.id);
}
var obj = {
    id: "awesome"
};
// obj就是这样的可选参数,用于接收foo内的this哦~
[1,2,3].forEach( foo, obj );// 1 awesome 2 awesome 3 awesome
[1,2,3].forEach( foo ); // 1 undefined 2 undefined 3 undefined

如果在全局加一个:var id = "global",再调用[1,2,3].forEach( foo ); 结果就是:

1 "global" 2 "global" 3 "global"

通过new绑定

最后一条军规了~

仍然是从我们可能犯错的地方出发思考。

重新定义js中的构造器

function foo(a) {
    this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2

这么干净的就宣布了thisfoo的了~

所以来认真思考下为什么~~

JS中,用new并不表示实例化了一个类,只不过恰巧和有类的语言同名了而已,没有其他特殊因素。

JS中的函数,包含内建函数,能够通过在函数名前加new,这会使得这类函数调用有个新的名字:constructor call. 大概翻译为构造器调用,而并非构造器。

这个区别很细微但很重要:实际没有一个构造器函数,只是一种构造器调用,具体构造啥呢?

当函数前面有new时,下面几条会自动执行:

  1. 一个全新的对象被建立
  2. 新建的对象是原型连接的
  3. 新建的对象被设定为绑定了this,意味着在该函数内使用this就是表示自身
  4. Unless the function returns its own alternate object, the new- invoked function call will automatically return the newly con‐structed object 这个还不知道如何理解呢

万物皆有序

现在四条军规已经揭晓,我们所需要做的就是找到调用点,并确定使用了哪个规则。

但是如果多条规则同时满足呢?这时候就需要个顺序了。

默认绑定顺序是最靠后的,这个毋庸置疑。

先看显式绑定和隐式绑定的顺序。

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3, 
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2

可见,显式绑定顺序在隐式绑定之前。

现在问题就只剩下一个了,new绑定和显式绑定谁先谁后?

还是通过代码来看:

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};
// 隐式绑定
obj1.foo(2); // 为this.a赋值2,但是你猜this现在指向谁呢?
console.log(obj1.a); // 2,为obj1添加了一个新属性

obj1.foo.call(obj2, 3); // 显式调用,并传递数据
console.log(obj2.a); // 3,this指向obj2

var bar = new obj1.foo(4); // new, this指向函数自身
console.log(obj1.a); // 2 
console.log(bar.a); // 4

new绑定顺序在隐式绑定之前。

但是newcall/apply不能一起用。

如何测出new绑定和显式绑定的顺序呢?

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1); // 强行赋予this给obj1
bar(2); // 
console.log(obj1.a); // 2

var baz = new bar(3); 
console.log(obj1.a); // 2, new修改了this的指向
console.log(baz.a); // 3

new绑定的顺序在显式绑定之前。

再看一个:

function foo(p1,p2) {
    this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");

baz.val; // "p1p2"

通过var bar = foo.bind(null, "p1")this绑定到了空对象上,但是通过new覆盖了this的绑定,结果是"p1p2",那么,这个p1是如何保留的呢?

关于这部分内容,仍然有待进一步思考理解才能完善。

目前只是一个大概的认知。

还有个softBind,暂时不深究。

箭头函数的this规则

在ES6中,箭头函数用的不是上面的四个规则。

function foo() {
    return (a) => {
        console.log( this.a ); 
    }
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);// 2

barfoo.call(obj1)的返回结果,返回的是箭头函数。

foo函数内创建的箭头函数,会在调用时捕获foo中的this

The lexical binding of an arrow-function cannot be overridden (even with new!).

function foo() {
    setTimeout(() => {
        console.log( this.a );
    });
}
var obj = {
    a: 2
};
foo.call( obj ); // 2

要注意箭头函数和前面四条规则的不同,程序中可以混用,但是在单个函数内可不要两种都出现。

ES6提出的箭头函数是在词法作用域进行的this绑定。就是说,箭头函数会从闭包函数继承this绑定。

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

推荐阅读更多精彩内容