五、事件处理函数的this
5.1 事件绑定
<button id="btn">点击我</button>function handleClick(e) {console.log(this);// <button id="btn">点击我</button>
}document.getElementById('btn').addEventListener('click', handleClick, false);
// <button id="btn">点击我</button>document.getElementById('btn').onclick = handleClick;
// <button id="btn">点击我</button>
根据上述代码我们可以得出:当通过事件绑定来给DOM元素添加事件,事件将被绑定为当前DOM对象。
5.2 内联事件
<button onclick="handleClick()" id="btn1">点击我</button>
<button onclick="console.log(this)" id="btn2">点击我</button>function handleClick(e) {console.log(this); // window
}
根据上述代码我们可以得出:点击btn1打印window对象,this指向window;点击btn2打印当前DOM对象,this指向当前DOM对象。
浏览器有三种添加事件监听的方式。
直接在标签内写 οnclick=“fn”,此时this指向window;
在js中 el.οnclick=fn,此时this指向el;
在js中用 el.attachEvent或者el.addEventListener(),此时this指向el。
六、定时器中的this
先看下面的两段代码,看看有何不同之处?
function foo() {setTimeout(function() {console.log(this); // window}, 1000)
}foo();
var name = "前端技术营";
var obj = {name: "张三",foo: function() {console.log(this.name); // 张三setTimeout(function() {console.log(this.name); // 前端技术营}, 1000)}
}obj.foo();
通过这两段代码发现,函数 foo 内部this指向为调用它的对象,即为obj 。而定时器中的this指向为 window。
如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的,但是平时很多场景下,都需要修改this的指向,那么有什么办法让定时器中的this跟包裹它的函数绑定为同一个对象呢?
(1)利用闭包,在外部函数中将this存为一个变量,回调函数中使用该变量,而不是直接使用this。
var name = "前端技术营";var obj = {name: "张三",foo: function() {console.log(this.name); // 张三var that = this;setTimeout(function() {console.log(that.name); // 张三}, 1000)}
}obj.foo();
(2)使用bind实现。
var name = "前端技术营";var obj = {name: "张三",foo: function() {console.log(this.name); // 张三setTimeout(function() {console.log(this.name); // 张三}.bind(this), 1000)}
}obj.foo();
七、箭头函数中的this
在使用普通函数之前对于函数的this绑定,需要根据这个函数如何被调用来确定其内部this的绑定对象。而且常常因为调用链的数量或者是找不到其真正的调用者对 this 的指向模糊不清。在箭头函数出现后其内部的 this 指向不需要再依靠调用的方式来确定。
箭头函数不绑定 this ,它只会从作用域链的上一层继承 this。箭头函数中的this是定义函数的时候绑定,而不是在执行函数的时候绑定。箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
看下面代码来对比普通函数与箭头函数中的this绑定。
var obj = {foo: function() {console.log(this); // obj},bar: () => {console.log(this); // window}
}obj.foo();
obj.bar();
上述代码中,同样是通过 obj. 方法调用一个函数,但是函数内部this绑定确是不同,只因一个数普通函数一个是箭头函数。
箭头函数中的this继承于作用域链上一层对应的执行上下文中的this。
var name = '前端技术营';
var obj = {name: '张三',foo: () => {console.log(this.name); // 前端技术营}
}
obj.foo();
this是继承于作用域链上一层执行上下文的this,这里的箭头函数中的this.name,箭头函数本身与foo平级是key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的上一层执行上下文就是window,因此这里的this.name实际上表示的是window.name,因此输出的是"前端技术营"。
var obj = {foo: function() {console.log(this); // objvar bar = () => {console.log(this); // obj}bar();}
}
obj.foo();
在普通函数中,bar 执行时内部this被绑定为全局对象,因为它是作为独立函数调用。但是在箭头函数中呢,它却绑定为 obj 。跟父级函数中的 this 绑定为同一对象。
var obj = {foo: () => {console.log(this); // windowvar bar = () => {console.log(this); // window}bar();}
}
obj.foo();
这个时候怎么又指向了window了呢?当我们找bar函数中的this绑定时,就会去找foo函数中的this绑定。因为它是继承于它的。这时 foo 函数也是箭头函数,此时foo中的this绑定为window而不是调用它的obj对象。因此 bar函数中的this绑定也为全局对象window。
箭头函数的 this 是从当前箭头函数逐级向上查找 this,如果找到了,则作为自己的 this 指向,如果没有则继续向上查找。而父级的 this 是可变的,所以箭头函数的 this 也可跟随父级而改变。
var name = "前端技术营";function Foo(val) {let bar = new Object();bar.name = val;bar.getVal = () => {console.log(this);};return bar;
}
let baz = new Foo("李四");
baz.getVal(); // Foo {}
八、return中的this
先看一段代码:
function Foo() { this.name = '张三'; return {};
}
var obj = new Foo();
console.log(obj.name); //undefined
再看一段代码:
function Foo() { this.name = '张三'; return function(){};
}
var obj = new Foo();
console.log(obj.name); //undefined
再看一段代码:
function Foo() { this.name = '张三'; return '1';
}
var obj = new Foo();
console.log(obj.name); // 张三
再看一段代码:
function Foo() { this.name = '张三'; return undefined;
}
var obj = new Foo();
console.log(obj.name); // 张三
通过上面的四段代码会发现什么?如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象,那么this还是指向函数的实例。
还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。
function Foo() { this.name = '张三'; return null;
}
var obj = new Foo();
console.log(obj.name); // 张三
九、被忽略的this
规则总有例外,这里也一样。如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,应用的是默认绑定规则。
var name = '张三';function foo() {// 张三,this指向windowconsole.log(this.name);
}
var obj = {name: '李四'
}
foo.call(null);
什么情况下会传入null呢?
function foo(a, b) {console.log("a:" + a + ", b:" + b);
}
// 把数组 “展开” 成参数
foo.apply(null, [2, 3]);// 使用 bind() 进行柯里化 (预先设置一些参数)
var bar = foo.bind(null, 2);
bar(3);
如果函数不关心 this 的话,你仍然需要传入一个占位值,这时 null 肯定是一个不错的选择,如果某个函数确实使用了 this,默认规则把 this 绑定到全局对象,这将导致不可预计的后果,比如修改全局对象。
十、this绑定优先级
显示绑定:通过使用.call()、.apply()或者.bind()等方法来明确指定函数内部的this值。这样就能将函数与特定的上下文进行绑定。
隐式绑定:当 函数引用 有 上下文对象 时,如 obj.foo() 的调用方式,foo() 内的 this 指向 obj。也就是说,谁调用函数,函数内的 this 就指向谁(无论是普通对象、还是全局对象),this 永远指向最后调用它的那个对象(不考虑箭头函数)。
默认绑定:表现为一个独立函数的直接调用,函数调用时无任何调用前缀的情景。
new绑定:new绑定就是通过new一个构造函数的方式进行绑定,this会指向被new出来的那个对象。
我们首先来看下隐式绑定和显示绑定哪个优先级更高。
function foo() {console.log(this.name)
}
var obj1 = {name: '张三',foo: foo
}
var obj2 = {name: '李四',foo: foo
}obj1.foo(); // 张三
obj2.foo(); // 李四obj1.foo.call(obj2); // 李四
obj2.foo.call(obj1); // 张三
这段到面可以看到,显示绑定优先级比隐式绑定更高。
现在我们需要搞清楚 new 绑定和隐式绑定的优先级谁高。
function foo(name) {this.name = name
}
var obj1 = {foo: foo
}
var obj2 = {}obj1.foo('张三');
console.log(obj1.name); // 张三obj1.foo.call(obj2, '李四');
console.log(obj2.name); // 李四var bar = new obj1.foo('王五');
console.log(obj1.name); // 张三
console.log(bar.name); // 王五
这段到面可以看到 new 绑定比隐式绑定优先高,但是 new 绑定和显示绑定谁的优先级高呢?
function foo(name) {this.name = name
}
var obj1 = {}var bar = foo.bind(obj1);
bar('张三');
console.log(obj1.name); // 张三var baz = new bar('李四');
console.log(obj1.name); // 张三
console.log(baz.name); // 李四
new 修改了硬绑定调用 bar() 中的 this。
this 存在很多使用场景,当多个场景同时出现时,就要根据优先级来判断 this 的指向。优先级:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
总结
-
定义在全局作用域中的普通函数中的this指向window对象: 作为函数调用,作为内部函数调用
setTimeout,setInterval。 -
事件处理函数中的this指向触发事件的标签元素 DOM对象绑定事件。
-
构造函数中的this指向当前正在创建的对象(严格模式下必须使用new)作为构造函数去调用,即new的时候,指向构造函数创建的那个实例 。
-
对象方法中的this指向:在使用对象方法调用方式时,this被自然绑定到该对象。