说说this

  我们都知道Javascript中的「this」真的是个头痛的东西,今儿我们就来好好总结下这个「this」。

  我记得之前这块内容,我直接先背了个「口诀」。

function fn() {
  console.log(this);
}

// 1. fn(); // 指向window
// 2. fn(); // undefined(严格模式下)
// 3. a.b.c.fn() // 指向a.b.c
// 4. new fn() // 指向new的实例对象
// 5. () => { fn(); } // 箭头函数调用fn,this指向「外层代码库的this」

之前了解的不多,今天来详细解析一波。

首先我们来说说this的绑定规则。
(1)默认绑定
(2)隐式绑定
(3)显式绑定
(4)new绑定

以下全部在「浏览器环境」中。

(1)默认绑定
  在不能应用「其他绑定规则」的时候,使用「默认绑定」,通常是作为「独立函数」进行调用。

function foo() {
    console.log(this.name); // 'Jason'
}

var name = 'Jason';

foo();

  在调用「foo()」的时候,应用了「默认绑定」,this指向了全局对象window(非严格模式下),严格模式下,指向「undefined」。

(2)隐式绑定
  函数的调用是在「某个对象」上触发的,即调用位置上存在「执行上下文」。典型的形式如「xxx.fn()」。

var name = 'Jack';

function fn() {
    console.log(this.name);
}

var obj = {

    name: 'Jason',

    foo: fn

};

obj.foo(); // 'Jason';

  函数fn的声明在对象obj的外部,看起来是不属于obj对象的,但是在调用foo的时候,隐式绑定会把函数调用中的「this」(foo函数中的this)绑定到对应的「执行上下文」(此例中的obj)。注意:对象属性链只有最后一层会影响调用位置。

function fn() {
    console.log(this.name);
}

let b = {
    name: 'Jack',
    foo: fn
}

let a = {
    name: 'Jason',
    friend: b
}

a.friend.foo();

  上面代码中,foo函数的执行环境不是a,而是「a.friend」,即是「b」。

  但是隐式绑定存在一个问题:绑定丢失。我们来看下边一段代码:

function fn() {
    console.log(this.name);
}

var name = 'Jack';

let obj = {
    name: 'Jason',
    sayName: fn,
}

let say = obj.sayName;

say(); // 'Jack'

  如果我们单看「obj.sayName」,执行上下文是对象obj,但是我们把「obj.sayName」赋值给了变量say后,调用say()后,函数fn的执行上下文就变为了全局变量(window)中。
  针对这类问题,我们只要记住,形式为「xxx.fn()」才是隐式绑定,如果格式为「fn()」,前面什么都没有,那肯定不是隐式绑定,但是也不一定是「默认绑定」,下文中会解释。
  除了上述的「绑定丢失」,还有一种绑定丢失的情况,就是发生在「回调函数」中,我们再看一个例子。

function fn() {
    console.log(this.name);
}

var person1 = {
    name: 'Jason',
    sayName: function() {
        setTimeout(function() {
            console.log('Hello!', this.name);
        })
    }
};

var person2 = {
    name: 'Jack',
    sayName: fn
};


var name = 'Tom';

person1.sayName(); // 'Hello! Tom'

setTimeout(person2.sayName, 100); // 'Tom'

setTimeout(function() {
    person2.sayName();  // 'Jack'
}, 200);

我们依次说说每次输出的原因。

  • (1)setTimeout的回调函数中,this是「默认绑定」,在非严格模式下,指向全局变量「window」,所以输出「'Hello! Tom'」
  • (2)第二条可能有些迷惑,不是说格式为「xxx.fn()」就是隐式绑定吗?然后执行上下文就是对象xxx?其实是这样的,对于setTimeout(fn, delay),第一个参数「fn」是「person2.sayName」,也就是说我们把「person2.sayName」赋值给了fn,然后执行了fn(),这样就跟person2无关系。
  • (3)第三条虽然也是在setTimeout函数中,但是我们可以看到执行的是「 person2.sayName()」,所以是一个隐式绑定,因此函数的执行上下文是person2,跟当前的作用域无关系。

(3)显式绑定
  显式绑定主要是通过call,apply和bind来显式的绑定this,call,apply和bind的第一个参数就是对应的this对象,call和apply的作用一样,只是call从第二个参数开始依次传入参数,而apply是直接把所有参数集成为一个数组放到第二个参数,bind会返回一个函数,在正式执行函数的时候,优先调bind第二个后的参数。来看看代码:

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
};


var name = 'Jack';

var sayName = person.sayName;

sayName.call(person); // 'Jason'

  上述代码中,如果先不看最后一行,看倒数第二行「var sayName = person.sayName」,通过上述的讲述,可以认定到这一行,如果直接调用,所处的执行上下文是全局变量window,但是最后一行的call函数,指定了this的对象为person,则输出‘Jason’。

  那么显式绑定是不是会出现绑定丢失呢?看下面的代码。

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn(); // 'Jack'
};

Hi.call(person, person.sayName);

  乍一看最后一行的显式绑定,确实Hi函数的this绑定到了对象person上,Hi函数会接受一个参数fn,然后执行fn();此刻这个参数fn是call的第二个参数「person.sayName」,但是在执行fn的时候,相当于直接调用了sayName函数(person.sayName赋值给了参数fn,隐式绑定也丢了,),对应的是默认绑定。

  那我们能不能继续还是想要绑定到person上?

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn.call(this); // 'Jason'
};

Hi.call(person, person.sayName);

  其实只用在Hi函数中对fn使用call调用,因为刚刚我们说了最后一句话,HI的this对象绑定到了person对象上,那么我们在调用fn函数的时候再次显示绑定一次this,此时「fn.call(this)」中的this就是Hi函数的this对象(person)。

(4)new绑定
  关于new会发生什么,可以具体看我之前的一篇文章(https://www.jianshu.com/p/6ea91eb41283
)。

  简单来说:
(1)创建一个空对象,作为将要返回的对象实例。
(2)将这个空对象的原型,指向构造函数的「prototype」属性。
(3)将这个空对象赋值给构造函数内的「this」关键字。
(4)开始执行构造函数内的代码。

function Person(name) {
    this.name = name;
}

var x = new Person('Jason');

console.log(x.name) // 'Jason'

(5)绑定例外
  上述(1)~(4)已经基本参数了this绑定的规则,但是我们还是要补充点可能存在的问题。比如「绑定例外」。

  如果我们在使用「显示绑定」的时候,第一个参数传了「null」或者「undefined」,这些值是会被忽略的,实际上应用的是「默认绑定」

(6)绑定优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

(7)箭头函数
  箭头函数是ES6中的语法,带来了很多的便利,但是我们也有几个重要的注意点:
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
  其中第一点就跟本文的主题this密切相关,我们来看两个例子。

var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: function() {
        console.log(this.name); // 'Jack'
    }
};

obj.sayName();
var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: () => {
        console.log(this.name); // 'Jason'
    }
};

obj.sayName();

  上述两个例子非常相似,唯一不同点在于obj的sayName函数一个使用了普通函数形式(第一个例子),另一个使用了箭头函数(第二个例子),然后输出结果就不同了。

  总的来说:普通函数的this是函数「运行时」绑定的,而箭头函数的this是函数「定义时」绑定的。

  这里我们怎么理解「定义时」?我们可以说箭头函数的this和其外层代码库的this一样,或者说指向其父级执行上下文中的this

上述第二个例子中,箭头函数本身跟sayName平级以key:value的形式,也就是说箭头函数本身存在于obj对象上,而obj对象所处的环境在全局变量window上,所以此例中的this.name其实是window.name。我们再看两个例子。

var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = function() {
        console.log(this.name); // 'Jason'
    };

    fn();

}

var x = new Test();
var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = () => {
        console.log(this.name); // 'Jack'
    };

    fn();

}

var x = new Test();

  第一个例子中,一个普通函数赋值给了变量fn,然后直接调用fn(),是默认绑定,此时的this指向全局变量window,所以this.name就是window.name,输出‘Jason’。
  第二个例子中,我们把箭头函数赋值为了变量fn,箭头函数中的this指向了父级执行上下文中的this,父级执行上下文中的this是通过new指向了其构造函数的实例(本例中的x),然后Test函数中第一句的「this.name = 'Jack'」,使得x.name为Jack。

  我们再来一个例子练练手。

var obj = {
    hi: function(){
        console.log('1', this); // obj
        return ()=>{
            console.log('2',this); // obj
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this); // window
            return ()=>{
                console.log('4', this); // window
            }
        }
    },
    say: ()=>{
        console.log('5', this); // window
    }
}

let hi = obj.hi();

hi();

let sayHi = obj.sayHi();

let fun1 = sayHi();

fun1();

obj.say();

  我们来依次解释一下:
(1)第一条(1处)是由于「obj.hi()」调用函数,此时是隐式绑定,固然1处的this指向了obj。
(2)第2处,由于调用「hi()」,obj.hi()返回的是一个箭头函数,这个箭头函数中的this与外层代码库的this一样,外层代码库就是obj对象中的fn属性(这是一个函数),于是乎,跟1处的this是一样的,都指向obj。
(3)由于「obj.sayHi()」赋值给了变量sayHi,「sayHi」执行的时候,相当于原本是隐式绑定然后变为默认绑定,固然3处输出的是window。
(4)由于sayHi()赋值给了fun1,在「fun1()」执行的时候,执行的是一个箭头函数,然后我们就找这个箭头函数的外层代码库的this,就与3处的this相同,即输出window。
(5)由于「obj.say()」的执行,乍一看是一个隐式绑定,但是看到函数是箭头函数,obj的外层代码库所在的环境就是全局变量window,输出window。

那么箭头函数一定是静态的吗?

var obj = {
    hi: function(){
        console.log('1', this);
        return ()=>{
            console.log('2', this);
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this);
            return ()=>{
                console.log('4', this);
            }
        }
    },
    say: ()=>{
        console.log('5', this);
    }
}

let sayHi = obj.sayHi();

let fun1 = sayHi(); // window
fun1(); // window

let fun2 = sayHi.bind(obj)(); // obj
fun2(); // obj

  还是用上面的例子,我们看看fun1和fun2,第一次执行「sayHi()」赋值给fun1的时候,是由隐式绑定转换为了默认绑定,this为window,执行「fun1」的时候,是执行箭头函数,所以箭头函数里的this也是window。
  但是对于fun2, 「sayHi.bind(obj)()」中使用bind显示绑定了sayHi的this对象为obj,本来没有bind绑定的时候,是由隐式绑定转变为默认绑定,然后此处我们又强行使用bind绑定回来了。所以「sayHi.bind(obj)()」执行的时候,3处的this为obj,然后「fun2()」执行的时候,4处的this和3处的this一样都是obj。

我们来一个终极题目来综合一下。

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}

var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

  我们来分析下这段代码,在obj对象的fn属性定义的时候,就是一个「立即执行函数」,并且带有「闭包」,我们看看这个「立即执行函数」中的this指向了谁?没有new绑定,没有显式绑定,没有隐式绑定,自然就是默认绑定了,this指向了全局变量window。所以「立即执行函数」中的代码可以这样理解。

var number; // number是undefined

window.number *= 2; // 此时全局变量中的number变为 10

number = number * 2; // 由于number是undefined, Number(undefined)为NaN,此处number变为NaN。
number = 3; // 然后又使变量number变为3

执行完fn的立即执行函数后。obj的fn属性是下列这样的:

fn: function() {
  var num = this.number;
  this.number *= 2;
  console.log(num);
  number *= 3;
  console.log(number);
}

然后我们执行「myFun.call(null)」,这种显式绑定我们在上述文章中说过,如果第一个参数为null,就是转为默认绑定。本例中就是执行obj的fn函数,然后我们再来每一行分析一波:我们首先想想obj的fn函数里面的this是谁?由于是默认绑定所以是window,于是乎。

var num = this.number; // 由于this指向了window,此时window.number为10,则num被赋值为10

this.number *= 2; // 使得全局变量的「number」,变为20

console.log(num); // 输出10

number *= 3; 这个number是obj.fn中的闭包函数中的「number」

console.log(number) ; // 同时输出这个number
改变闭包中的number变量

接着我们执行「obj.fn()」,还是优先执行了obj.fn的立即执行函数。立即执行函数的this还是指向全局变量window,然后依次分析每行代码。立即执行函数中:

var number;

this.number *= 2; // 等价于 window.number *= 2; 使得全局变量的number变为20

number = number * 2; // NaN

number = 3;

然后我们在执行「立即执行函数」返回的函数。此时由于是「obj.fn()」这样调用,所以this指向了obj对象。

var number = this.number; // 3,即obj.number

this.number *= 2; // 使得obj.number变为6

console.log(number); // 输出3

number *= 3; // 此时number还是指闭包中的那个「number」,刚刚number是9,现在就是27了

console.log(number); // 就是输出闭包那个「number」,是27

最后执行「console.log(window.number)」,此时全局变量的number是20,输出20.

参考:

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

推荐阅读更多精彩内容