一、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种绑定规则,下面对这些种规则分别进行介绍:
- 默认绑定规则: 默认指向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
文章图片
回调函数中(定时函数内部的回调函数,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绑定【在用构造函数实例化对象的时候】
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();
//Avar 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。
再贴几道题加深印象:
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(立即调用函数表达式)
推荐阅读
- JavaScript|JavaScript之函数(二) 函数内部三个不常见的属性
- 小程序知识|微信小程序js.json.wxss.wxml后缀文件的区别及使用方法
- 使用jQuery下拉隐藏显示
- 前端|青龙面板仓库推荐 以及修复拉库失败(2.20更新)
- contact form 7(”提交”按钮在Twitter Bootstrap Modal中不起作用)
- 前端软件系列--curl
- 如何使用具有Google Maps集成的jaredpalmer/presspack构建主题()
- 前端面试资料整理【工具篇】
- 如何一次向所有wp帖子添加功能