有人纵横题海 有人this指向整不明白

一、this指向绑定规则

在js中,this是函数当前的运行环境,它表示当前对象的一个引用;这个当****前对象很迷,它的具体值会随着执行时所在环境的改变而改变。但是,有一个不会变的点:this永远指向它所在函数的实际调用者,如果没有调用者,就指向全局对象【浏览器环境:window;node环境:global;ES5之后严格模式下值为undefined】

严格模式只是让函数内部的this值为undefined,并不会影响window对象本身的值

为什么会有this问题?

ECMAScript中定义了对象属性的(js中一切皆对象)的4种数据属性和2种访问器属性(getter,setter函数),这4种数据属性分别是分别是:

configurable:是否可重新配置定义,能否删除;

enumberable:是否可枚举,是否可用for…in遍历对象属性;

writable:是否可写;

value:属性值(默认undefined),声明变量不赋值的时候默认用的就是这个默认值undefined。

其他这里不赘述,我们的主角是value属性。

熟悉基本类型和引用类型的同学都知道,下面代码段中,JS引擎在内存生成一个对象,并将该对象的内存地址赋值给变量obj。要读取obj.a的值时,JS引擎先从obj拿到对象的内存地址,然后找到原始对象并返回属性a的值。属性a的值保存在a的属性描述对象的value属性中,则a的数据属性value是1;

var obj = {
  a: 1,
  foo: function() {
    //do sth
  }
}

同样,foo属性中保留的也是函数的地址,函数是被单独保存在内存中的,foo保存了函数的地址[即foo的数据属性value中保存的是该函数的地址];让我们换一种写法:

function foo(){
  //do sth
}
var obj = {
  a: 1,
  foo
}

函数foo是一个独立的值,我们可以通过不同的方式访问它从而使它在不同的环境执行

foo(); //全局环境
obj.foo(); //obj环境

JS允许我们在函数体中引用当前环境的其他变量。下列代码中,函数foo中引用了变量a,a由foo的运行环境提供。

function foo() {
  console.log(a)
}

var a = 'window';
var obj = {
  a: 'obj',
  foo:foo
}

我们已经知道函数是可以运行在不同上下文(执行环境),我们需要一种机制获取函数内部当前上下文(执行环境),而this的出现,就是为了指示函数的当前运行的环境。

function foo() {
  console.log(this.a)
}
var a = 'window';
var obj = {
  a: 'obj',
  foo:foo
}
foo(); //全局环境中,this.a指向全局的a
obj.foo(); //obj环境中,this.a指向obj环境的a

那么如何区分出函数的执行环境呢?这个this到底是谁呢?且看看this的绑定规则。

在非箭头函数中this的指向存在4种绑定规则,下面对这些种规则分别进行介绍:

  1. 默认绑定规则: 默认指向window, 函数独立调用时指向全局对象
// 默认指向全局对象
console.log("1",this);//Window or global(node) 
console.log(this === window);//tru

下面列举一些函数被独立调用的场景:

//直接调用function声明的函数,this指向Window对象
function foo(){console.log("2",this);}
foo(); 
window.foo();

将匿名函数赋值给某个变量,再通过该变量调用,也属于独立调用的情况。此时this指向Window对象,严格模式下为undefined

var foo = function() {
   // "use strict";
    console.log("3",this); 
}
foo();//window
//IIFE,【(function(){})(); or (function(){}())】其实也可以拆解成上面类似的场景,
//声明一个匿名函数,立即调它,匿名函数的调用具有全局性,this指向Window对象
(function() {
    console.log("4",this);
})()
//闭包【当函数执行的时候,导致函数被定义并被抛出就会产生闭包】中:
varobj = {
    a: 'aaa',
    b: function() {
        function c() {
            console.log('c---',this); //window
        }
        return c;
    }
}
obj.b()(); 
//函数c是在这里才被调用的,【obj.b()返回函数c】相当于独立调用c,所以this指向window

2. 隐式绑定规则:谁调用就指向谁(通过对象.函数属性() 方式调用,函数中的this指向该对象)this是在执行函数时才确定的。

通过obj.funAttr()调用,函数funAttr()中this指向obj

//对象的方法调用,this指向对象本身
var obj = {
    aa: this,  //对象属性中,this指向window
    func: function() {
      console.log(this); //obj        
      function a(){
        console.log(this);//window
      }
      a(); //因为在这里函数是独立调用的,内部this指向window
    }
}
obj.func();

当对象链式调用时,this指向最靠近函数的对象

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

obj1.obj2.foo(); //obj2

共享实例prototype中的this指向调用它的实例对象

String.prototype.logthis = function () {
    return "我是"+this+", "+this+"真棒hahaha"

}
console.log("溪森堡".logthis()) //我是溪森堡, 溪森堡真棒hahaha

Function.prototype.logthis2 = function () {
    return this; //指向add函数            
}
var add = function(x,y) {
 return x+y;
} 
add.logthis2();

事件绑定和监听函数(非箭头函数)中,this指向被绑定/监听的元素对象

var btn = document.getElementById("btn");
btn.onclick = function() {
    console.log(this); //
}
btn.addEventListener('click', function() {
    console.log(this); //
})

内联事件中this指向:

a. 内联处理函数调用代码时,this指向监听器对应的DOM元素;

b. 代码被包含在函数内部执行(直接调用)时,非严格模式指向全局对象window,严格模式指向undefined




图片

回调函数中(定时函数内部的回调函数,forEach,sort等)this默认指向全局对象window;

函数foo在定时器中是作为回调函数来执行的,回到主线程执行时在是全局执行环境中执行,涉及Event Loop这里暂且不细说,setTimeout内部可拆解成下面这样,这时候this指向window

setTimeout(callback) {
    if (回调条件满足) (
        callback()
    )
}

隐式绑定中还有一个比较特殊的例子:

var length = 10;
function fn() {
    console.log(this.length);
}

var obj = { 
    a:'a',
    length: 5,
    method: function(fn) { 
      fn(); //10
      arguments[0](); //5
  }
};

obj.method(fn, 1, 2, 3, 'a');

fn()独立调用函数,没有疑问this指向window,输出10;

arguments[0]()这个有点奇怪,arguments[0]不就是fn么,那不也是相当与fn()吗?4哪来的?

我们先回顾一下什么是arguments。

MDN中的定义:arguments 是一个对应于传递给函数的参数的类数组对象。

说白了就是实际参数所组成的类数组对象(要拿到形参个数可用函数名.length);而类数组,并不是真正的数组,它可以通过length属性拿到实参个数,并且这些参数是以0开始为key,实参值为value的键值对的形式呈现的。它与真正的数组不同,它没有数组内置函数如forEach()、map()等。

有人纵横题海 有人this指向整不明白_第1张图片

我们回到上面arguments[0]()的问题,obj.method(fn, 1, 2, 3, ‘a’); 函数method内部得到的arguments对象结构如下:

{

0: fn,

1: 1,

2: 2,

3: 3,

4: ‘a’

}

注意,我们说这是一个类数组对象,我们知道访问对象属性的方法有:obj.attr 和 obj[attr]

有人纵横题海 有人this指向整不明白_第2张图片

把这里的arguments看成obj,是不是就明晰了,这里其实就是通过obj[‘attr’]的方式调用fn,发生了隐式绑定,fn中的this指向了arguments对象,它同时是一个类数组,具有length属性,所以this.length为实际参数个数,也就是5。

再看一个关于隐式绑定的例子:

var a='a';
function foo(){
    console.log(this.a);
}
var obj1 = {
    a: 'obj1',
    foo: function(){
        setTimeout(function(){
            console.log(this.a);
        },0)
    }
}
var obj2 = {
    a: 'obj2',
    foo
}
var obj3 = {
    a: 'obj3',
    foo
}

obj1.foo(); //a,回调函数中
setTimeout(obj2.foo, 0); //a,或掉函数中,且是独立调用的
setTimeout(function(){
    obj3.foo(); //obj3,虽然在回调函数中,但是是通过对象调用,隐式绑定
},0);

如果我们需要this指向对应的objXXX对象,可以这样做:

a. 提前保留this的值

var obj1 = {
    a: 'obj1',
    foo: function(){
        let that = this;
        setTimeout(function(){
            console.log(that.a);
        },0)
    }
}
obj1.foo(); //obj1

b. 强制改变this指向(后面会提到,显式绑定)

setTimeout(obj2.foo.bind(obj2), 0);  //obj2

像forEach等高阶函数最后一个可选参数可指定this指向

有人纵横题海 有人this指向整不明白_第3张图片

在高阶函数中,父函数是可以决定子函数中this指向的

new bar(fn) {
  fn();
  new fn();
  fn.bind(obj)();
}

在隐式绑定规则中,有两种情况会导致隐式绑定失败,此时this绑定规则变成默认绑定

a. 变量赋值的情况【隐式丢失】

function foo() {
  console.log('foo---',this); //window
}
var obj = {
  a: 'aaa',
  b: foo
}
obj.b(); //obj
//隐式丢失(变量赋值情况)
var bar = obj.b; //这里只是引用了函数,拿到存放函数的地址而已
bar(); //window, 在这里才调用的,相当于函数独立调用

b.参数赋值的情况, 在预编译过程中,实参被赋值为形参是一个值拷贝的过程(浅拷贝,这里指拷贝函数的引用地址)

function foo() {
    console.log('foo---',this); //window
}
function bar(fn) {
    fn();
}
var obj = {
    a: 'aaa',
    foo
}
bar(obj.foo);

3. 显式绑定规则: 用call/bind/apply显式地为一个函数指定其this指向,这里不赘述用法以及区别 后面专门写一篇文章介绍。

function foo() {
  console.log(this);
}
var obj1 = {
  attr: 1
}
var obj2 = {
  attr:2
}
foo(); //window
foo.call(obj1); //obj1
foo.call(obj2); //obj2
  1. new绑定【在用构造函数实例化对象的时候】

了解过new内部实现原理的同学都知道,new操作符构造函数其实内部干了这几件事情:

a. 创建一个空的js对象{};

b. 为在步骤a创建的对象添加__proto__属性并将该属性指向构造函数的protptype;

c. 改变this指向并传递参数(给构造函数绑定新构造的对象为this,给新对象添加对应的属性和方法);

d. 如果构造函数返回了对象,直接返回该对象,否则返回上述步骤构造的新的实例对象。

function Foo(a) {
  this.a = a;
  console.log('Foo---',this);//new绑定中this指向实例对象 p
  return this;
  //当构造函数中返回基本类型的值,this仍然是指向实例对象
  // return 1;

  //当构造函数中返回基本引用值,this被改变成指向该引用值
  //  return {};
}
var p = new Foo('aa')

new绑定规则中,this指向实例对象p。 如果构造函数返回的不是一个对象,则p为一个新构造出来的对象;如果返回的是一个对象,p指向构造函数返回的对象。

有人纵横题海 有人this指向整不明白_第4张图片

function P (name, age) {
  this.name = name;
  this.age = age;
  this.foo = function () {
    console.log(this.name)
  }
  this.bar = function () {
    return function () {
      console.log(this.age)
    }
  }
}
var name = 'xsb';
var age = 18;
var p = new P('xx', 100)
p.foo() //new绑定 构造函数中this指向p,输出xx
console.log(p.age); //100
p.bar()() //独立调用返回的函数,this指向window, 输出18

二、上述规则的优先级

上面讲述了this指向的四种绑定规则,当两种或多种规则存在的情况下,谁的优先级更高?接下来通过几个例子验证一下上述四种规则的优先级:

1. 显式绑定优先级大于隐式绑定大于默认绑定

var obj1 = {
    attr: 'aaa',
    func: test
}
var obj2 = {
    attr: 'bbb',
    func: test
}
function test() {
    console.log(this);
}
test() //默认,window
obj1.func() //隐式,obj1
obj1.func.call(obj2) //显式,obj2

2. new绑定优先级比显式绑定高

function foo(attr) {
  this.attr = attr;
}
var obj = {};
//显式绑定,bind拷贝原函数并指定了this指向,bar和foo不是同一个函数了
var bar = foo.bind(obj); //bar 是通过foo.bind返回的一个函数 this指向obj
    bar(2);
    
    console.log(obj.attr); //2
    //以bar为构造函数构造一个实例对象,是一个新的对象,跟obj没有任何关系
    var baz = new bar(3); //指向实例对象baz;
    console.log(obj.attr); //2
    console.log(baz.attr); //3

第6行代码等同于 var baz = new (foo.bind(obj))(3); 最后baz.attr输出3说明this并非绑定obj而是绑定了new操作符构造出的新的实例对象;

综上得出结论:

new绑定规则 > 显式绑定规则 > 隐式绑定规则 > 默认绑定规则


三、箭头函数中this指向

在es6的箭头函数出现以前,上述绑定规则是普适的,但是在你看到箭头函数时,请将上面的每一句话都忘掉!

箭头函数中的this指向取决于其父环境中的this指向【箭头函数内部是没有this的,它直接拿定义时的上下文(this指向)作为当前this指向,而不是使用时所在的作用域指向的对象】

独立调用非箭头函数,函数嵌套this不继承父作用域上下文,指向全局对象window;

functionfoo() {
    console.log(this); // obj
    function test() {
        console.log(this); //window
    }
    test();
}

var obj = {
    a: 1,
    foo
}

obj.foo();

箭头函数中this只由父作用域决定,哪怕这里test()是独立调用的。

functionfoo() {
    console.log(this); // obj
    var test = () => {
        console.log(this); //obj
    }
    test();//独立调用箭头函数
}

var obj = {
    a: 1,
    foo
}
obj.foo();

foo函数执行完成,返回函数test

function foo(){
  console.log(this); //window
  function bar(){
    console.log(this);  //obj2
  }
  return bar;
}

var obj = {
  attr: '111',
  foo
}
var obj2 = {
  attr: '222',
  foo
}
var baz = foo().call(obj2);

显式绑定不能对箭头函数起作用:

function foo(){
  console.log(this); //window
  var bar = () => console.log(this);  //window
  return bar;
}

var obj = {
  attr: '111',
  foo
}
var obj2 = {
  attr: '222',
  foo
}
var baz = foo().call(obj2);

new绑定无效,甚至会直接报错,箭头函数不能作为构造函数

var Foo = () => console.log(this);
new Foo();

有人纵横题海 有人this指向整不明白_第5张图片

var A = {
  name: 'A',
  say: function() {
    var s = () => console.log(this.name);
    return s;
  }
}

var sayHello = A.say(); //隐式绑定,say方法中this指向A;
//执行say方法返回的一个箭头函数s,this由say方法中作用域对象this决定,故输出A
sayHello();  //A

var B = {
  name: 'B'
}
//显式绑定对箭头函数无效,故只要say方法中this不变,下列输出也一直是A
sayHello.call(B); //A
sayHello.call(); //A

最后做一道练习综合回顾下,

有人纵横题海 有人this指向整不明白_第6张图片

运行一下,看看你做对了几个,下面是解析:

  1. 通过对象obj1调用一个普通函数,this指向obj1,属于隐式绑定,输出1;

  2. 为方法fn1显式地绑定this指向obj2,输出2;

  3. fn2是一个箭头函数,其this指向定义位置的父作用于this即window,输出window;

  4. 同3,显式绑定对箭头函数不起作用,输出window;

  5. 函数fn3执行之后返回一个普通函数,相当于独立调用fn3,this指向window;

  6. 为fn3执行之后返回的函数显式地绑定this指向obj2,shuchu 2;

  7. 为fn3绑定this指向obj2,但是独立调用了其返回的函数,该函数this窒息那个window;

  8. fn4是一个普通函数,执行之后返回箭头函数。通过对象方法调用,fn4中this窒息那个obj1,再执行其返回的箭头函数,this由其父作用于fn4中this决定,输出1;

  9. fn4中this指向obj1(隐式绑定),显式绑定对其返回的箭头函数无效,故还是输出1;

  10. fn4的this被显式绑定成obj2,其返回的箭头函数中this由父级作用域决定。输出2。

由此可见,要改变箭头函数中this的值,只能通过更改其父作用域指向的对象—this来实现。

再贴几道题加深印象:

var name = 'global';
var obj = {
    name: 'local',
    foo: function(){
        this.name = 'foo';
    }.bind(window) // 显式绑定
};
var bar = new obj.foo(); // new绑定,优先级比显式高,this指向bar,构造函数中给bar对象增加了name属性,值为foo,输出foo
setTimeout(function() {
    console.log(window.name);// global
    console.log(this.name); // 回调函数中this指向window,输出global
}, 0);
console.log(bar.name); // foo

var bar3 = bar2 = bar;
bar2.name = 'foo2';
console.log(bar3.name);// foo2, bar3,bar2,bar保存的都只是对象的地址,改变了实际对象的
var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3 //IIFE,this指向window,60
  console.log('num---',num) //20
  num++ //20++
    return function (n) {
        this.num += n
        console.log('num===',num)
        num++
        console.log(num)
    }
  })(obj.num) // IIFE,定义的时候就会执行
var fn = obj.fn //函数别名,保存函数的引用
fn(5) 
obj.fn(10) // this指向obj
console.log(num, obj.num)

这里将一个立即执行函数赋值给obj.fn,将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。obj.fn和fn的数据属性value其实都是指向下列函数的地址。

ƒ (n) {
    this.num += n
    console.log('num===',num)
    num++
    console.log(num)
  }

fn(5) 独立调用函数,this指向window,this.num即window.num,window.num在IIFE中被修改成60。 故this.num = 60 + 5 即65

num++, IIFE中被修改成21,则是21++; 再次访问num的时候值已经被修改成22,故输出22;

obj.fn(10) 隐式绑定,上面代码块中this指向obj; this.num = 20 + 10  即30;

num的值在上一次函数调用值被修改成22, num++即22++,输出num为23;

console.log(num, obj.num) 直接访问全局的num, 在执行fn(5)的时候window.num被修改成65,故第一项输出65; obj.num 参考上一步解析,输出30

相关资料:

《2w字大章 38道面试题》彻底理清JS中this指向问题

JavaScript的this原理-阮一峰

JS中return一个函数与直接return一个函数变量的区别

IIFE(立即调用函数表达式)

你可能感兴趣的