有人纵横题海 有人this指向整不明白
Posted hst_gogogo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了有人纵横题海 有人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种绑定规则,下面对这些种规则分别进行介绍:
- 默认绑定规则: 默认指向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); //<button id="btn">点我</button>
btn.addEventListener('click', function()
console.log(this); //<button id="btn">点我</button>
)
内联事件中this指向:
a. 内联处理函数调用代码时,this指向监听器对应的DOM元素;
b. 代码被包含在函数内部执行(直接调用)时,非严格模式指向全局对象window,严格模式指向undefined
<button onclick="console.log(this)">click here</button>
<button onclick="(function()console.log(this))()">click here</button>
<button onclick="(function()'use strict';console.log(this))()">click here</button>
回调函数中(定时函数内部的回调函数,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()等。
我们回到上面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]
把这里的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指向的
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
- 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指向构造函数返回的对象。
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();
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
最后做一道练习综合回顾下,
运行一下,看看你做对了几个,下面是解析:
-
通过对象obj1调用一个普通函数,this指向obj1,属于隐式绑定,输出1;
-
为方法fn1显式地绑定this指向obj2,输出2;
-
fn2是一个箭头函数,其this指向定义位置的父作用于this即window,输出window;
-
同3,显式绑定对箭头函数不起作用,输出window;
-
函数fn3执行之后返回一个普通函数,相当于独立调用fn3,this指向window;
-
为fn3执行之后返回的函数显式地绑定this指向obj2,shuchu 2;
-
为fn3绑定this指向obj2,但是独立调用了其返回的函数,该函数this窒息那个window;
-
fn4是一个普通函数,执行之后返回箭头函数。通过对象方法调用,fn4中this窒息那个obj1,再执行其返回的箭头函数,this由其父作用于fn4中this决定,输出1;
-
fn4中this指向obj1(隐式绑定),显式绑定对其返回的箭头函数无效,故还是输出1;
-
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(立即调用函数表达式)
以上是关于有人纵横题海 有人this指向整不明白的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript - this指向问题(建议收藏反复查看)