JavaScript this 指向 - 练习题 - 详细解析

38 阅读9分钟

基础题解析(1-10)

1. 全局调用

var a = 1;
function test() {
  console.log(this.a);
}
test();  // 输出: 1

解析

  • 普通函数直接调用
  • 在非严格模式下,this指向全局对象(浏览器中是 window,Node.js 中是 global
  • var a = 1会挂载到全局对象上
  • 所以 this.a就是 window.aglobal.a

2. 对象方法调用

var obj = {
  a: 2,
  test: function() {
    console.log(this.a);
  }
};
obj.test();  // 输出: 2

解析

  • 普通函数作为对象的方法调用
  • this指向调用该方法的对象 obj
  • 所以 this.a就是 obj.a

3. 方法赋值后调用

var a = 1;
var obj = {
  a: 2,
  test: function() {
    console.log(this.a);
  }
};
var foo = obj.test;  // 只是赋值函数引用
foo();  // 输出: 1

解析

  • obj.test只是将函数引用赋值给 foo
  • foo()是普通函数调用,不是方法调用
  • this指向全局对象
  • 所以 this.a是全局的 a

4. 多层对象

var a = 1;
var obj1 = {
  a: 2,
  obj2: {
    a: 3,
    test: function() {
      console.log(this.a);
    }
  }
};
obj1.obj2.test();  // 输出: 3

解析

  • 函数作为对象的方法调用
  • 调用者是 obj1.obj2
  • this指向直接调用者 obj2
  • 所以 this.aobj2.a

5. 回调函数

var a = 1;
var obj = {
  a: 2,
  test: function() {
    console.log(this.a);
  }
};
setTimeout(obj.test, 100);  // 输出: 1

解析

  • setTimeout的回调函数是 obj.test的函数引用
  • 相当于:setTimeout(function() { console.log(this.a); }, 100)
  • 回调函数是普通函数调用
  • this指向全局对象(浏览器中是 window
  • 严格模式下可能是 undefined

验证

// 这相当于:
var callback = obj.test;  // 取出函数
setTimeout(callback, 100);  // 普通函数调用

6. 构造函数

function Person(name) {
  this.name = name;
  console.log(this);  // 输出: Person { name: 'Alice' }
}
var p = new Person('Alice');

解析

  • 使用 new调用构造函数
  • this指向新创建的实例对象
  • 创建的对象是 Person的实例
  • 实例的 __proto__指向 Person.prototype

7. 箭头函数

var a = 1;
var obj = {
  a: 2,
  test: () => {
    console.log(this.a);
  }
};
obj.test();  // 输出: 1

解析

  • 箭头函数没有自己的 this
  • this继承自定义时的作用域
  • 在全局作用域定义对象字面量this指向全局对象
  • 所以箭头函数内的 this指向全局对象

关键箭头函数的 this在定义时确定,不是在调用时确定

8. 嵌套箭头函数

var a = 1;
var obj = {
  a: 2,
  test: function() {
    return () => {
      console.log(this.a);
    };
  }
};
obj.test()();  // 输出: 2

解析

  1. obj.test()调用时,testthis指向 obj
  2. 返回的箭头函数继承外层函数 testthis
  3. 箭头函数的 thistest函数的 this,即 obj
  4. 所以输出 obj.a即 2

9. 方法中的函数

var a = 1;
var obj = {
  a: 2,
  test: function() {
    function inner() {
      console.log(this.a);
    }
    inner();
  }
};
obj.test();  // 输出: 1

解析

  • obj.test()调用时,testthis指向 obj
  • inner是普通函数,不是箭头函数
  • inner()是普通函数调用,this指向全局对象
  • 这就是为什么需要箭头函数或 bind来保持 this

解决方案

// 方案1:使用箭头函数
test: function() {
  const inner = () => {
    console.log(this.a);  // 2
  };
  inner();
}

// 方案2:使用 bind
test: function() {
  const inner = function() {
    console.log(this.a);  // 2
  }.bind(this);
  inner();
}

// 方案3:保存 this
test: function() {
  const self = this;
  function inner() {
    console.log(self.a);  // 2
  }
  inner();
}

10. 立即执行函数

var a = 1;
var obj = {
  a: 2,
  test: (function() {
    console.log(this.a);  // 立即执行,输出: 1
  })()
};
// 只定义 obj,test 是 undefined

解析

  • 立即执行函数 (IIFE) 是普通函数调用
  • 在非严格模式下,this指向全局对象
  • 输出全局的 a即 1
  • 注意:obj.test的值是 undefined,因为 IIFE 没有返回值

进阶题解析(11-20)

11. call/apply

var a = 1;
var obj1 = { a: 2 };
var obj2 = { a: 3 };

function test() {
  console.log(this.a);
}

test.call(obj1);  // 输出: 2
test.apply(obj2);  // 输出: 3

解析

  • callapply可以显式绑定 this
  • test.call(obj1)this绑定到 obj1
  • test.apply(obj2)this绑定到 obj2
  • 区别:call接收参数列表,apply接收参数数组

12. bind

var a = 1;
var obj = { a: 2 };

function test() {
  console.log(this.a);
}

var boundTest = test.bind(obj);
boundTest();  // 输出: 2

var obj2 = { a: 3, test: boundTest };
obj2.test();  // 输出: 2(不是 3!)

解析

  • bind创建新函数,永久绑定 this
  • boundTestthis被永久绑定到 obj
  • 即使通过 obj2.test()调用,this仍然是 obj
  • bind的绑定优先级最高

13. bind + new

function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function() {
  console.log(this.name);
};

var obj = {};
var BoundPerson = Person.bind(obj);
var p = new BoundPerson('Alice');

p.sayName();  // 输出: "Alice"
console.log(obj.name);  // 输出: undefined

解析

  1. Person.bind(obj)创建了绑定函数 BoundPerson
  2. 但使用 new调用时,bind的绑定会被忽略
  3. new BoundPerson('Alice')创建新实例 p
  4. pname是 'Alice'
  5. objname属性不存在,是 undefined

关键规则new的优先级高于 bind

14. 严格模式

'use strict';
var a = 1;
function test() {
  console.log(this);  // 输出: undefined
}
test();

解析

  • 严格模式下,普通函数调用的 thisundefined
  • 非严格模式下才是全局对象
  • 这是严格模式的重要安全特性

15. 箭头函数 + call

var a = 1;
var obj = { a: 2 };
var test = () => {
  console.log(this.a);
};
test.call(obj);  // 输出: 1

解析

  • 箭头函数没有自己的 this
  • 箭头函数无法通过 callapplybind改变 this
  • 箭头函数的 this继承自定义时的作用域
  • 这里是在全局作用域定义的,this指向全局对象
  • 输出全局的 a即 1

16. 事件处理器

// 假设在浏览器中
var button = document.createElement('button');
button.textContent = 'Click me';

var obj = {
  a: 1,
  handleClick: function() {
    console.log(this.a);
  }
};

button.addEventListener('click', obj.handleClick);
button.click();  // 输出: undefined

解析

  • 事件处理器中,this指向触发事件的 DOM 元素
  • 这里 this指向 button元素
  • button.aundefined
  • 所以输出 undefined

如果想要输出 1

// 方案1:使用 bind
button.addEventListener('click', obj.handleClick.bind(obj));

// 方案2:使用箭头函数
button.addEventListener('click', () => obj.handleClick());

// 方案3:修改 handleClick
handleClick: function(e) {
  console.log(obj.a);  // 直接引用 obj
}

17. 类中的 this

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayName() {
    console.log(this.name);
  }
  
  sayNameArrow = () => {
    console.log(this.name);
  }
}

const p = new Person('Bob');
const { sayName, sayNameArrow } = p;
sayName();      // 输出: undefined(错误!)
sayNameArrow(); // 输出: "Bob"

解析

  1. sayName()

    • 是普通方法,添加到 Person.prototype
    • 单独调用时,this指向 undefined(严格模式)
    • 所以 this.nameundefined
  2. sayNameArrow

    • 是箭头函数,在构造函数中定义
    • 作为实例属性,不是原型方法
    • 箭头函数继承构造函数中的 this
    • 即实例 p,所以输出 "Bob"

解决方案

// 正确调用方法
p.sayName();  // 输出: "Bob"

// 或使用 bind
const boundSayName = p.sayName.bind(p);
boundSayName();  // 输出: "Bob"

18. 原型链上的 this

function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log(this.name);
};

var p1 = new Person('Alice');
var p2 = new Person('Bob');

p1.sayName();  // 输出: "Alice"
p2.sayName();  // 输出: "Bob"

解析

  • 原型方法中的 this指向调用该方法的实例
  • p1.sayName()thisp1
  • p2.sayName()thisp2
  • 所有实例共享同一个原型方法,但 this不同

19. 多层 bind

var obj1 = { a: 1 };
var obj2 = { a: 2 };
var obj3 = { a: 3 };

function test() {
  console.log(this.a);
}

var test1 = test.bind(obj1);
var test2 = test1.bind(obj2);
var test3 = test2.bind(obj3);

test3();  // 输出: 1

解析

  • bind只能绑定一次
  • 后续的 bind不会改变已经绑定的 this
  • test1this被永久绑定到 obj1
  • 所以最终输出 obj1.a即 1

20. 综合题

var length = 10;
function fn() {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function(fn) {
    fn();  // 1️⃣
    arguments[0]();  // 2️⃣
  }
};

obj.method(fn, 1, 2, 3);
// 输出: 10
// 输出: 4

解析1️⃣ fn()输出: 10

  • 普通函数调用,this指向全局对象
  • 全局 length是 10
  • 浏览器中 window.length表示 iframe 数量
  • Node.js 中 global.lengthundefined,但这里用 var定义了全局变量

2️⃣ arguments[0]()输出: 4

  • 特殊调用方式:arguments[0]()相当于 arguments.fn()

  • 这里的 this指向 arguments对象

  • arguments.length是传入的参数个数

  • obj.method(fn, 1, 2, 3)传入了 4 个参数

  • 但注意:arguments[0]fn函数

  • 所以相当于:

    arguments.fn = fn;
    arguments.fn();  // this 指向 arguments
    
  • 实际上浏览器中 arguments.length是 4,因为:

    • 第 1 个参数是 fn
    • 第 2-4 个参数是 1, 2, 3
  • 但需要在实际环境中验证

实际验证

var length = 10;
function fn() {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function(fn) {
    console.log("fn():");
    fn();  // 10
    
    console.log("arguments:", arguments);
    console.log("arguments.length:", arguments.length);  // 4
    console.log("arguments[0]:", arguments[0]);  // fn 函数
    
    console.log("arguments[0]():");
    arguments[0]();  // 4
  }
};

obj.method(fn, 1, 2, 3);

挑战题解析(21-25)

21. 链式调用

var obj = {
  value: 1,
  increment: function() {
    this.value++;
    return this;  // 关键:返回 this
  },
  getValue: function() {
    return this.value;
  }
};

console.log(obj.increment().increment().getValue());  // 输出: 3

解析

  • 链式调用的关键是每个方法都返回 this
  • obj.increment()thisobj
  • 返回 this即返回 obj
  • 所以可以继续调用 .increment()
  • 最终 value从 1 增加到 3

22. 模块模式

var calculator = (function() {
  var value = 0;  // 私有变量
  
  return {
    add: function(x) {
      value += x;
      return this;  // 支持链式调用
    },
    getValue: function() {
      return value;
    },
    reset: function() {
      value = 0;
    }
  };
})();

calculator.add(5).add(3);
console.log(calculator.getValue());  // 输出: 8

解析

  • IIFE 创建闭包,value是私有变量
  • 返回对象中的方法,this指向返回的对象
  • add方法返回 this,支持链式调用
  • 方法通过闭包访问私有变量 value

23. 数组方法中的 this

var obj = {
  data: [1, 2, 3, 4, 5],
  multiplier: 2,
  multiply: function() {
    return this.data.map(function(item) {
      return item * this.multiplier;  // 问题:这里的 this
    });
  }
};

console.log(obj.multiply());  // 输出: [NaN, NaN, NaN, NaN, NaN]

解析

  • map的回调函数是普通函数
  • 回调函数中的 this默认指向全局对象
  • 严格模式下是 undefined
  • 所以 this.multiplierundefined
  • item * undefined得到 NaN

24. 修复数组方法中的 this

var obj = {
  data: [1, 2, 3, 4, 5],
  multiplier: 2,
  multiply: function() {
    return this.data.map(function(item) {
      return item * this.multiplier;
    }, this);  // 关键:传入 thisArg
  }
};

console.log(obj.multiply());  // 输出: [2, 4, 6, 8, 10]

解析

  • 数组方法(mapforEachfilter等)可以接收第二个参数 thisArg
  • thisArg会作为回调函数中的 this
  • 这里传入 this,即外层的 obj
  • 所以回调函数中的 this.multiplierobj.multiplier

其他解决方案

// 方案1:使用箭头函数
multiply: function() {
  return this.data.map(item => item * this.multiplier);
}

// 方案2:保存 this
multiply: function() {
  const self = this;
  return this.data.map(function(item) {
    return item * self.multiplier;
  });
}

// 方案3:使用 bind
multiply: function() {
  return this.data.map(function(item) {
    return item * this.multiplier;
  }.bind(this));
}

25. Proxy 中的 this

var target = {
  value: 1,
  getValue: function() {
    return this.value;
  }
};

var handler = {
  get: function(obj, prop) {
    if (prop === 'value') {
      return 100;
    }
    return obj[prop];
  }
};

var proxy = new Proxy(target, handler);
console.log(proxy.getValue());  // 输出: 100

解析

  1. proxy.getValue()调用
  2. Proxy 的 get捕获器返回 target.getValue
  3. 然后调用 target.getValue()this指向 target
  4. target.value经过 Proxy 的 get 捕获返回 100
  5. 所以输出 100

注意:Proxy 不改变方法的 this绑定 如果想让 this指向 proxy

var handler = {
  get: function(obj, prop) {
    if (prop === 'value') {
      return 100;
    }
    const value = obj[prop];
    if (typeof value === 'function') {
      return value.bind(obj);  // 或者返回函数包装器
    }
    return value;
  }
};

this 绑定规则总结

调用方式this 指向优先级
new构造函数新创建的实例1
call/apply/bind指定的对象2
对象方法调用调用该方法的对象3
普通函数调用全局对象/undefined4
箭头函数定义时的作用域的 this不参与比较

记忆口诀

  • 普通函数:
    • new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  • 箭头函数:
    • 看定义,不看调用

实际应用建议

  1. 使用箭头函数处理回调函数中的 this
  2. 避免滥用箭头函数,特别是需要作为构造函数的方法
  3. 类方法使用普通函数,实例方法可以考虑箭头函数
  4. 使用 bind​ 提前绑定 this,避免运行时错误
  5. 严格模式下更安全,普通函数调用的 thisundefined