关于this

221 阅读5分钟

思考题

请输出下列代码

1.非严格模式-指向window

function foo() {
    console.log(this.a);
}
var a = 2;
foo();
展开答案 输出:2

分析: 在非严格模式下, 函数内的 this 是 指向windowvar a 声明了一个全局变量

2.严格模式-指向undefined

'use strict';
function foo() {
    console.log(this.a);
}
var a = 2;
foo();
展开答案 输出:Uncaught TypeError: Cannot read property 'a' of undefined

3. 隐式绑定-指向对象

function foo() {
  var a = 1;
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
};
obj.foo();
展开答案 输出:2

4. 隐式绑定-指向对象

function foo() {
  console.log(this.a);
}
var obj2 = {
  a: 42,
  foo: foo
};
var obj1 = {
  a: 2,
  b:foo,
  obj2: obj2
};
obj1.obj2.foo(); 
obj1.b(); 
foo();
展开答案 依次输出:42
依次输出:2
依次输出:undefined

5. 非严格模式-隐式绑定-隐式丢失

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo1: foo
};
obj.foo1()
var bar = obj.foo1; // 函数别名!
var a = "哈哈,你猜我在哪里呢?"; // a 是全局对象的属性
bar(); 
展开答案 依次输出:2
依次输出:哈哈,你猜我在哪里呢?

6. 隐式绑定-隐式丢失

function foo() {
 console.log(this.a);
}
var obj = {
 a:2,
 foo:foo
}
function setTimeout(fn,delay){
 fn();
}
var a = "哈哈,猜猜我在哪里呢?";
setTimeout(obj.foo,100);
展开答案 输出:哈哈,你猜我在哪里呢?

7. 误解-this指向函数的作用域

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); 
展开答案 输出:ReferenceError: a is not defined

请看下面改写的代码:


var a; // 把变量 a 放在全局作用域下声明
function foo() {
    a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); // 输出2

为什么要用this

  • 因为this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。

this到底是什么

  • 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。

误解

this 指向函数的作用域

  • 在某种情况下它 是正确的,但是在其他情况下它却是错误的。

this指向自身

  • this 指向自身的观点是错误的
  • 在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用
  • 接受this,不要刻意回避它的写法
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身

foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4

调用位置

  • this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用
  • 调试技巧, 浏览器 => f12 => 在函数的代码处设置断点 => 运行代码 => 调试器在断点处暂停,显示调用栈列表(call stack)

绑定规则

默认绑定

  • 非严格模式,使用默认绑定时, this 指向全局对象。
  • 严格模式(strict mode),全局对象对象无法使用默认绑定, this 会绑定到 undefined

隐式绑定

代码如下:

function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

显式绑定

  • call
  • apply

new绑定

  • new绑定发生了什么
  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

优先级

  • 显式绑定优先级更高

绑定例外

被忽略的this

间接引用

softBind

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
    var fn = this;
    // 捕获所有 curried 参数
    var curried = [].slice.call( arguments, 1 );
    var bound = function() {
    return fn.apply(
    (!this || this === (window || global)) ?
    obj : this
    curried.concat.apply( curried, arguments )
    );
    };
    bound.prototype = Object.create( fn.prototype );
    return bound;
    };
}


function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了softBind

this词法

  • this 既不指向函数自身也不指向函数的词法作用域

注意事项

  1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数
  3. 尽量不要在代码中混合使用 strict mode 和 non-strict mode。整个程序要么严格要么非严格。如果使用第三方库,其严格程度和你的代码有所不同,要注意兼容性细节。

总结

  1. 由 new 调用?绑定到新创建的对象
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局
  5. ES6 中的箭头函数 并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样

参考资料

  • 书籍-《你不知道的JavaScript(上卷)》