简析JavaScript中的最复杂的机制之一——this

this是JavaScript中最复杂的机制之一,被自动定义在所有函数的作用域中。“When a function of an object was called , the object will be passed to the execution context as 'this' value.”当一个函数被调用时,拥有这个函数的对象会作为this传入,所以this可以是全局对象,当前对象乃至任何对象
例如:

function f(){
    var name = "Funny";
    console.log(this.name);  //undefined
    console.log(this);   //window
}
f();    //f();与window.f();效果相同

调用函数f,因为函数f是最外层的函数,所以它拥有全局作用域,换句话说,它是全局对象(window)的一个方法(全局变量是全局对象的属性或方法)。console.log(this.name),程序执行到这里时,会对全局作用域进行RHS查询名为name的变量,由于不存在,所以控制台输出‘undefined’,console.log(this),输出全局对象window。
 为什么要用this这个关键字?倘若我们不用this:

var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
function identify(context){
    return context.name.toUpperCase();
}

function speak(context){
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}
identify(you);  //READER
speak(me);  //Hello,I'm KYLE

如果不用this来传递上下文对象,而采用显式传递上下文对象,这会让代码变得越来越混乱,尤其当使用模式越来越复杂的时候(上面的例子代码特别简单)。函数自动引用合适的上下文对象十分重要:

var you = {
    name: "Reader";
};
var me = {
    name: "Kyle"
};
function identify(){
    return this.name.toUpperCase();
}
function speak(){
    var greeting = "Hello,I'm " + identify.call(this);
    console.log(greeting);
}
identify.call(you); //READER
speak.call(me); //Hello,I'm KYLE

this并非指向函数本身

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0;i < 10; i++){
    if(i > 5){
        foo(i); //foo: 6;foo: 7;foo: 8;foo: 9;
    }
}
console.log(foo.count);     //0

按道理,count是计算函数foo调用的次数,foo.count = 0的确为函数对象foo添加一个名为count的属性,但是foo函数内部的this.count中的this并非指向foo函数对象,而是指向window(全局对象)。在非严格模式下,this.count++;,会对count进行LHS查询,然而并没有在全局作用域中找到,所以就创建一个名为count的全局变量(严格模式下,程序会抛出引用异常);随着函数foo的调用,全局对象的属性count就递增,而函数对象的属性count则不变化,当然,console.log(count);会输出4。
如果要从函数对象内部引用它本身,那么只使用this是不够的。我们可以强制this指向foo函数本身:

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
    if(i > 5){
        foo.call(foo,i);
    }
}
console.log(foo.count);     //4

大多数情况下this并非指向函数的作用域
this在任何情况下都不会指向函数的词法作用域,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过JS代码访问,它存在于JS引擎内部。下面是个经典的错误例子:

function foo(){
    var a = 2;
    this.bar();
}
function bar(){
    console.log(this.a);
}
foo();  //ReferenceError: a is not defined

这段代码首先通过this.bar()来引用函数bar(意外的成功了),通常省略前面的this,此外,开发者还试图用this联通foo()和bar()的词法作用域,使得bar()可以访问foo()作用域中的变量a,当然这是不可能的。至于为什么抛出这样一个异常,这里就不再赘述了。
this到底是一样什么样的机制???
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。(动态作用域的定义瞬间浮现在脑海里)。当一个函数被调用时,会创建一个活动记录(有时也叫执行上下文)。这个纪录会包含函数在哪里被调用(调用栈)、函数调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

调用位置

要理解this的绑定过程,首先要理解调用位置。


D86C4F31-669D-4A41-BFFF-8A7A4647B25D.png

下面的例子中的注释就分析了调用栈,显然易懂,不再赘述。

function baz(){
    //当前调用栈:baz
   //当前调用位置:全局作用域
   console.log("baz");
   bar();// <-- bar的调用位置
}
function bar(){
    //当前调用栈:baz->bar
    //当前调用位置:baz
    console.log("bar");
    foo(); <--foo的调用位置
}
function foo(){
    //当前调用栈:baz->bar->foo
    //当前调用位置:bar
    console.log("foo");
}
baz(); <-- baz的调用位置

绑定规则

首先找到调用位置,然后判断需要按照哪条绑定规则进行应用,如果多条规则都适用,则按优先级。

默认绑定

最常见的函数调用类型:独立函数调用

function foo(){
    console.log(this.a);
}
var a = 0;
foo();      //0

函数foo调用时应用了默认绑定,this指向了全局对象window。foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能应用默认绑定严格模式下,则不能将全局对象用于默认绑定,因此this会被绑定到undefined

隐式绑定

调用位置是否有上下文对象?是否被某个对象拥有或者包含? function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); //2 无论直接在obj中定义还是先定义在添加为引用属性,函数foo严格上来说都不属于这个对象,但是在函数被调用的时候,可以认为obj拥有或者包含这个函数,调用位置会使用obj上下文来引用函数。当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象。所以调用foo()时,this被绑定到obj,因而这里的this.a和obj.a就是一样的。对象属性引用链只有上一层在调用位置起作用
被隐式绑定的函数会丢失绑定对象,然后它应用默认绑定(非严格模式下)。

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    //fn其实引用的是foo
    fn();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops,global";
doFoo(obj.foo); //“oops,global"

参数传递是一种隐式赋值,所以这种会造成隐式丢失(显式赋值也会造成隐式丢失)。

显式绑定

使用函数的call(...)和apply(...)方法。它们的第一个参数是对象,显然是为this准备的,在调用函数时,将它绑定到this。

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

显式绑定一样无法解决之前的丢失绑定问题,但显式绑定的变种可以解决这个问题。

硬绑定
function foo(){
    console.log(this.a);
}
var obj = {
    a;2
};
var bar = function(){
    foo.call(obj);
};
bar();  //2
setTimeout(bar,100);    //2
//硬绑定的bar是无法再修改它的this
bar.call(window);   //2

首先我们创建了一个函数bar(),并在它的内部调用了foo.call(obj),所以我们强制把foo的this绑定到了obj。之后无论怎么调用函数bar,它都会再一次手动地在obj上调用foo。硬绑定是一种非常常用的内置方法。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);
var b =  bar(3);    //2 3;
console.(b);    //5
new绑定

使用new来调用函数,会自动执行如下操作:
1、构造一个新对象
2、这个新对象会被执行[[Protorype]]连接
3、这个新对象会被绑定到函数调用的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

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

使用new来调用函数foo,我们会构造一个新对象并把它绑定到foo调用中的this上。

优先级

正常情况:new绑定>显式绑定>隐式绑定>默认绑定
特殊情况:当把null或者undefined作为绑定对象传入call、apply、bind,这些操作会被忽略,最后应用默认绑定。创建一个函数的“间接引用”,即前面提的绑定丢失,最终也是应用默认绑定。

软绑定

给默认绑定指定一个全局对象和undefined以外的值,那么就可以实现和硬绑定一样的效果,同时保留隐式绑定或显示绑定修改this的能力。

if(!Function.prototype.softBind){
    Function.prototype.softBind = function(obj){
        var fn = this;
        //捕获所以curried参数
        var curried = [].slice.call(arguments,1);
        var bound = function(){
            return fn.apply(
                (!this || this === (window || global))?
                    obj:this,
                curried.concat.apply(curried,arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
}

首先检查调用时的this,如果this绑定到全局对象或undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。

ES6中的新玩法

箭头函数不是用function关键字定义的,而是用操作赋 => 定义的,这是ES6中新增的语法糖之一。箭头函数不适用于this的绑定规则,而是根据外层作用域来决定this,无论最外层绑定到了什么,它都会继承下来。
首先箭头函数的词法作用域:

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

foo()内部的箭头函数会捕获调用foo()时的this,这里是obj1,所以bar的this绑定到了obj1,箭头函数的绑定无法修改。
箭头函数常用于回调函数中,例如事件处理器或定时器:

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

推荐阅读更多精彩内容

  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 6,922评论 15 54
  • 1.函数调用栈和调用位置 在函数执行的时候,会有一个活动记录(也叫执行上下文)来记录函数的调用顺序,这个就是函数调...
    lightNate阅读 517评论 1 14
  • 1. this之谜 在JavaScript中,this是当前执行函数的上下文。因为JavaScript有4种不同的...
    百里少龙阅读 986评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • “就是要宠你宠你宠上了天”,每一次让你逗我开心,你总是用一些歌词,我总是嫌弃你没有诚意,然而你的行动却比作词人来...
    Dontsayhellosay阅读 139评论 0 0