this 全面解析(笔记)
this 既不指向函数自身,也不指向函数的词法作用域。它实际上是在函数被调用时发生的绑定,指向什么取决于函数的调用位置或调用方法。
调用位置不是声明位置。有些编程模式可能会隐藏真正的调用位置。
使用浏览器的调试工具,可以查看调用栈,调用栈的第二个元素,就是真正的调用位置。
function baz(){ //当前调用栈是:baz //因此,当前调用位置是全局作用域 console.log("baz"); bar();// 这是bar 的调用位置 } function bar(){ //当前调用栈是:baz -> bar //因此,当前调用位置在baz中 console.log("bar"); } function foo(){ //当前调用栈是:baz -> bar -> foo //因此,当前调用位置在bar中 console.log("foo"); } baz();// 这是baz的调用位置
以上就是函数执行过程中的调用位置。
this有四条绑定规则。
规则1. 默认绑定。
最常用的函数调用类型:独立函数调用。
function foo(){ console.log(this.a); } var a = 2; foo(); // 2
调用foo()时候,this.a 被解析成了全局变量 a。函数调用时应用了this的默认绑定,因此this指向全局对象。
如果使用严格模式,那么全局对象无法使用默认绑定,this就会绑定到 undefined。
function foo(){ "use strict"; console.log(this.a); } var a = 2; foo(); // TypeError: Cannot read property 'a' of undefined
this 的绑定规则完全取决于调用位置,但是只有在非严格模式下,默认绑定才能绑定到全局对象。
规则2. 隐式绑定
函数调用位置是否有上下文对象。
function foo() { console.log(this.a); } var obj={ a:2, foo:foo }; obj.foo(); //2
foo()被当作引用属性添加到obj中。调用位置使用obj上下文来引用函数,隐式绑定规则将函数调用中的this绑定到这个上下文对象:obj。因此this.a和obj.a是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。比如:
function foo() { console.log(this.a); } var obj2={ a:42, foo:foo }; var obj1={ a:2, obj2:obj2 }; obj1.obj2.foo(); //42
隐式丢失:
一个最常见的this绑定问题就是隐式绑定的函数会丢失绑定对象。它会应用默认绑定,从而把this绑定到全局对象或者undefined上。取决于是否使用严格模式。
function foo() { console.log(this.a); } var obj={ a:2, foo:foo }; var bar = obj.foo; var a = "全局对象"; bar(); //"全局对象"
虽然bar是obj.foo的一个引用,但是实际上,引用的是foo函数本身,此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
另外一种情况发生在传入回调函数时:
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj={ a:2, foo:foo }; var a = "全局对象"; doFoo(obj.foo); //"全局对象"
参数传递其实就是一种隐式赋值,因此传入函数时也会被隐式赋值。
如果把函数传入语言内置的函数而不是自己声明的函数,结果也是一样的:
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj={ a:2, foo:foo }; var a = "全局对象"; setTimeout(obj.foo, 100); //"全局对象"
回调函数丢失this绑定是非常常见的。除此之外,调用回调函数的函数可能会修改this。一些流行的javascript库中,事件处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。
由于回调函数的执行方式无法控制,影响绑定的调用位置也就没有办法控制。那么如何固定this来修复呢。
规则3. 显示绑定
使用函数的call()和apply()方法,可以在某个对象上强制调用函数。这两个方法的第一个参数是一个对象,它们会把这个对象绑定到this,接着做调用函数时指定这个this。
function foo() { console.log(this.a); } var obj={ a:2 }; foo.call(obj); //2
显示绑定也无法解决前面的丢失绑定问题。但是显示绑定的一个变种可以解决这个问题:
3.1 硬绑定
function foo(something) { console.log(this.a, something); return this.a+something; } var obj={ a:2 }; var bar = function () { return foo.apply(obj, arguments); }; var b=bar(3); //2 3 console.log(b); //5
创建函数bar(),在内部手动调用foo.apply(),强制把foo的this绑定到了obj。之后无论如何调用函数bar,它总会手动在obj上调用foo。这就是显示的强制绑定。
另外一个例子,创建一个 i 可以重复使用的辅助函数:
function foo(something) { console.log(this.a, something); return this.a+something; } //简单的辅助绑定函数 function bind(fn, obj) { return function () { return fn.apply(obj, arguments); }; } var obj={ a:2 }; var bar = bind(foo, obj); var b=bar(3); //2 3 console.log(b); //5
ES5提供内置方法:Function.prototype.bind,因此可以这样:
function foo(something) { console.log(this.a, something); return this.a+something; } var obj={ a:2 }; var bar = foo.bind(obj); var b=bar(3); //2 3 console.log(b); //5
bind()会返回一个硬编码的新函数,会把参数设置为this的上下文并调用原始函数。
2.2 API调用的“上下文”
javascript的内置函数,基本都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind()一样。确保你的回调函数使用指定的this。
function foo(ele) { console.log(ele, this.id); } var obj={ id: "awesome" }; ['a','b','c'].forEach(foo,obj); //a "awesome" b "awesome" c "awesome"
调用foo()时把this绑定到obj。
规则4. new 绑定
function Foo(a) { this.a = a; } var bar = new Foo(2); console.log(bar.a); //2
使用new来调用Foo()时,我们会构造一个新对象并把它绑定到foo()调用中的this上。如果函数没有返回其他对象,new表达式中的函数调用会自动返回这个新对象。