javascript|有人纵横题海 有人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

【javascript|有人纵横题海 有人this指向整不明白】那么如何区分出函数的执行环境呢?这个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()) //我是溪森堡, 溪森堡真棒hahahaFunction.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

javascript|有人纵横题海 有人this指向整不明白
文章图片

回调函数中(定时函数内部的回调函数,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()等。
javascript|有人纵横题海 有人this指向整不明白
文章图片

我们回到上面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]
javascript|有人纵横题海 有人this指向整不明白
文章图片

把这里的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指向
javascript|有人纵横题海 有人this指向整不明白
文章图片

在高阶函数中,父函数是可以决定子函数中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指向构造函数返回的对象。
javascript|有人纵横题海 有人this指向整不明白
文章图片

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();

javascript|有人纵横题海 有人this指向整不明白
文章图片

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

最后做一道练习综合回顾下,
javascript|有人纵横题海 有人this指向整不明白
文章图片

运行一下,看看你做对了几个,下面是解析:
  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); // foovar 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(立即调用函数表达式)

    推荐阅读