一、调用位置

1、什么调用位置?

调用位置就是函数在代码中被调用的位置(而不是声明的位置)

2、如何寻找函数被调用的位置?

关键:分析调用栈,即为了到达当前执行位置所调用的所有函数。而我们关心的调用位置就在当前正在执行的函数的前一个调用中
先来看一段代码:

  1. function baz() {
  2. //当前调用栈是:baz
  3. // 因此,当前调用位置是全局作用域
  4. console.log("baz");
  5. bar(); // bar的调用位置
  6. }
  7. function bar() {
  8. // 当前调用栈是baz -> bar
  9. // 因此,当前调用位置在baz中
  10. console.log("bar");
  11. foo(); // foo的调用位置
  12. }
  13. function foo() {
  14. // 当前调用栈是baz -> bar -> foo
  15. // 因此,当前调用位置在bar中
  16. console.log("foo");
  17. }
  18. baz(); // <-- baz的调用位置

我们可以把调用栈想象成一个函数调用链,但这种方法麻烦且易出错。
但我们可以使用另一种方式:使用浏览器的调试工具,设立断点,或直接在代码中插入debugger。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。真正的调用位置是栈中的第二个元素

二、绑定规则

1、默认绑定

最常用的函数调用类型是独立函数调用。可把这规则看做是无法应用其他规则时的默认规则。
先看一段代码:

  1. function foo() {
  2. //当前调用栈是:baz
  3. // 因此,当前调用位置是全局作用域
  4. console.log(this.a);
  5. }
  6. var a = 2;
  7. foo(); // 2

从代码中发现this指向了全局对象,而且函数调用时应用了this的默认绑定。
如何判断是默认绑定?
可从分析调用位置来看看foo()是如何调用的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能是默认绑定,无法应用其他规则
但如果是在严格模式下,又会有怎样的结果呢?请看如下代码:

  1. function foo() {
  2. "use strict"
  3. console.log(this.a);
  4. }
  5. var a = 2;
  6. foo(); // TypeError:this is undefined

这段代码表示:虽然this的绑定规则完全取决于调用位置,但只有在非严格模式下,默认绑定才绑定全局对象;在严格模式下则会绑定到undefined。
但是在严格模式下调用则不影响默认绑定:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var a = 2;
  5. (function() {
  6. "use strict"
  7. foo(); // 2
  8. })();

注意:通常来说不应该在代码中混合使用strict模式与非strict模式

2、隐式绑定

这条规则是指调用位置是否有上下文对象,或者是否被某个对象拥有或包含
先看以下代码:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. obj.foo();// 2

该调用位置使用了obj上下文来引用函数,或者说函数被调用时obj对象“拥有”或“包含”它。
因此当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
上述代码调用foo()时,this被绑定到obj,因此this指向了obj,this.a 与 obj.a 是一样的。
另外对象属性引用链中只有上一层或最后一层在调用位置中起作用。例如:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj2 = {
  5. a: 42,
  6. foo: foo
  7. };
  8. var obj1 = {
  9. a: 2,
  10. obj2: obj2
  11. };
  12. obj1.obj2.foo();// 42

隐式丢失
被隐式绑定的函数会丢失绑定对象这是一个常见的this绑定问题,也就是说丢失后它会应用默认绑定,从而把this绑定到全局对象或undefined上,取决于是否是严格模式。
例1:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var bar = obj.foo; // 函数别名
  9. var a = "oops, global";// a是全局对象的属性
  10. bar(); // "oops, global"

虽然bar是obj.foo的引用,但却引用了foo函数的本身,此时的bar()是不带任何修饰的函数调用,因此使用了默认绑定
例2:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. function doFoo(fn) {
  5. // fn其实引用的是foo
  6. fn(); // 调用位置
  7. }
  8. var obj = {
  9. a: 2,
  10. foo: foo
  11. };
  12. var a = "oops, global";// a是全局对象的属性
  13. doFoo(obj.foo); // "oops, global"

这里使用了参数传递,也是隐式赋值,所以结果和例1一样
例3:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var a = "oops, global";// a是全局对象的属性
  9. setTimeout(obj.foo, 100);// oops, global

回调函数丢失this绑定是常见的,调用回调函数的函数可能会修改this
总结: 分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上

3、显式绑定

方法:可以使用call或apply直接指定this的绑定对象
缺点:无法解决丢失绑定的问题
例:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj = {
  5. a: 2
  6. };
  7. foo.call(obj); // 2

如果你传入了一个原始值作为this绑定对象,这个原始值会被转换成它的对象形式(new xxx()),这叫装箱

(1)、硬绑定

此为显式绑定的一个变种,可以解决丢失绑定问题
缺点:会大大降低函数的灵活性,使用绑定之后就无法使用隐式绑定或者显式绑定来修改this
例:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj = {
  5. a: 2
  6. };
  7. var bar = function() {
  8. foo.call(obj);
  9. }
  10. bar(); // 2
  11. setTimeout(bar, 100); // 2
  12. // 硬绑定的bar不可能再修改它的this
  13. bar.call(window); // 2

foo.call(obj)强制把this绑定到了obj,之后调用函数bar,它总会在obj上调用foo,这是显式的强制绑定,叫做硬绑定
典型应用场景一:创建一个包裹函数,负责接收参数并返回值

  1. function foo(something) {
  2. console.log(this.a, something);
  3. return this.a + something;
  4. }
  5. var obj = {
  6. a: 2
  7. };
  8. var bar = function() {
  9. return foo.apply(obj, arguments);
  10. }
  11. var b = bar(3); // 2 3
  12. console.log(b); // 5

典型应用场景二:创建一个可以重复使用的辅助函数

  1. function foo(something) {
  2. console.log(this.a, something);
  3. return this.a + something;
  4. }
  5. // 简单的辅助绑定函数
  6. function bind(fn, obj) {
  7. return function() {
  8. return fn.apply(obj, arguments)
  9. }
  10. }
  11. var obj = {
  12. a: 2
  13. };
  14. var bar = bind(foo, obj);
  15. var b = bar(3); // 2 3
  16. console.log(b); // 5

由于硬绑定是一种常用模式,所以ES5提供了内置方法Function.prototype.bind:

  1. function foo(something) {
  2. console.log(this.a, something);
  3. return this.a + something;
  4. }
  5. var obj = {
  6. a: 2
  7. };
  8. var bar = foo.bind(obj);
  9. var b = bar(3); // 2 3
  10. console.log(b); // 5

bind会返回一个硬编码的新函数,会把你指定的参数位置为this的上下文并调用原始函数

(2)、API调用“上下文”

通过 call() 或 apply() 实现

4、new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面操作
a、创建一个全新对象
b、新对象会被执行[[Prototype]]链接
c、新对象被绑定到函数调用的this
d、如果函数没有返回其他对象,则自动返回新对象
代码:

  1. var obj = {};
  2. obj.__proto__ = Base.prototype;
  3. var result = Base.call(obj);
  4. return typeof result === 'obj' ? result : obj;

三、优先级

1、隐式绑定与显式绑定

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var obj1 = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var obj2 = {
  9. a: 3,
  10. foo: foo
  11. };
  12. obj1.foo(); // 2
  13. obj2.foo(); // 3
  14. obj1.foo.call(obj2); // 3
  15. obj2.foo.call(obj1); // 2

显然:显式绑定 > 隐式绑定

2、new绑定与隐式绑定

  1. function foo(something) {
  2. this.a = something;
  3. }
  4. var obj1 = {
  5. foo: foo
  6. };
  7. var obj2 = {};
  8. obj1.foo(2);
  9. console.log(obj1.a);// 2
  10. obj1.foo.call(obj2, 3); // 3
  11. console.log(obj2.a);// 3
  12. var bar = new obj1.foo(4);
  13. console.log(obj1.a);// 2
  14. console.log(bar.a);// 4

new绑定 > 隐式绑定

3、new绑定与显式绑定

new和call/apply无法一起使用,因此无法通过new foo.call(obj1) 来直接测试,但我们可以使用硬绑定来测试

  1. function foo(something) {
  2. this.a = something;
  3. }
  4. var obj1 = {};
  5. var bar = foo.bind(obj1);
  6. bar(2);
  7. console.log(obj1.a);// 2
  8. var baz = new bar(3);
  9. console.log(obj1.a);// 2
  10. console.log(bar.a);// 3

这里bar被硬绑定在了obj1上,但new bar(3)并没有把obj1.a修改为3。相反,new修改了硬绑定(到obj1的)调用bar()中的this。因为使用了new绑定,我们得到了一个名为baz的新对象,并且baz.a的值为3
new绑定 > 硬绑定(显式绑定)

4、判断this

(1)、由new调用? 绑定到新创建的对象(new绑定)

  1. var bar = new foo();

(2)、由call或apply或bind调用?绑定到指定对象(显式绑定)

  1. var bar = foo.call(obj2);

(3)、由上下文对象调用?绑定到那个上下文对象(隐式绑定)

  1. var bar = obj1.foo();

(4)、默认绑定:严格模式下绑定到undefined,否则为全局对象

  1. var bar = foo();

四、绑定例外

1、被忽略的this

如果你把null货undefined作为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际应用默认绑定规则:

  1. function foo() {
  2. console.log(this.a);
  3. }
  4. var a = 2;
  5. foo.call(null); // 2
  1. function foo(a, b) {
  2. console.log("a:"+ a + ", b:" + b);
  3. }
  4. foo.apply(null, [2, 3]);// a:2, b:3
  5. var bar = foo.bind(null, 2);
  6. bar(3); // a:2, b:3

总是用null来忽略this绑定可能会产生一些副作用。如果某个函数使用了this(如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(浏览器中为window),这会导致不可预计的后果(如修改全局对象),或者导致更多难以分析和追踪的bug
更安全的this
一种更安全的做法是传入一个特殊对象,把this绑定到这个对象不会对你的程序产生任何副作用。
可创建一个”DMZ”非军事区对象,一个空的非委托的对象,任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响

  1. function foo(a, b) {
  2. console.log("a:"+ a + ", b:" + b);
  3. }
  4. // 我们的DMZ空对象
  5. var __null = Object.create(null);
  6. foo.apply(__null, [2, 3]);// a:2, b:3
  7. var bar = foo.bind(__null, 2);
  8. bar(3); // a:2, b:3

2、间接引用

间接引用的情况下,调用这个函数会应用默认绑定规则,并且最容易在赋值时发生:

  1. function foo(a, b) {
  2. console.log(this.a);
  3. }
  4. var a = 2;
  5. var o = { a: 3, foo: foo };
  6. var p = { a: 4 };
  7. o.foo();// 3
  8. (p.foo = o.foo)(); // 2

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo() 而不是p.foo()或o.foo(),这里会使用默认绑定
对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式

3、软绑定

给默认绑定指定一个全局对象和undefined以外的值,可实现和硬绑定相同的效果,同时保留隐式绑定或显式绑定修改this的能力

  1. if(!Function.prototype.softBind) {
  2. Function.prototype.softBind = function(obj) {
  3. var fn = this;
  4. // 捕获所有curried参数
  5. var curried = [].slice.call(arguments, 1);
  6. var bound = function() {
  7. return fn.apply(
  8. (!this || this === (window || global)) ?
  9. obj : this,
  10. curried.concat.apply(curried, arguments)
  11. );
  12. };
  13. bound.prototype = Object.create(fn.prototype);
  14. return bound;
  15. }
  16. }
  1. function foo(a, b) {
  2. console.log("name: " + this.name);
  3. }
  4. var obj = { name: 'obj' },
  5. obj2 = { name: 'obj2' },
  6. obj3 = { name: 'obj3' };
  7. var fooOBJ = foo.softBind(obj);
  8. fooOBJ(); // name: obj
  9. obj2.foo = softBind(obj);
  10. obj2.foo(); // name: obj2
  11. fooOBJ.call(obj3); // name: obj3
  12. setTimeout(obj2.foo, 100); // name: obj 使用了软绑定

从上述代码中可以看到软绑定版本的foo()可以手动将this绑定到obj2或obj3上,但如果应用默认绑定,则会将this绑定到obj

五、箭头函数

箭头函数不使用this的四种标准规则,而是根据外层(函数或全局)作用域来决定this
箭头函数的几个注意点:
(1)箭头函数没有自己的this对象(详见下文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的
箭头函数导致this总是指向函数定义生效时所在的对象(或者箭头函数的this指向是定义是的this不是运行时的this,即父级作用域下的this)

  1. function foo() {
  2. // 返回一个箭头函数
  3. return (a) => {
  4. // this继承自foo();
  5. console.log(this.a);
  6. };
  7. }
  8. var obj1 = {
  9. a: 2
  10. };
  11. var obj2 = {
  12. a: 3
  13. };
  14. var bar = foo.call(obj1);
  15. bar.call(obj2);
  16. //=====================================================================================
  17. //箭头函数导致this总是指向函数定义生效时所在的对象
  18. function foo() {
  19. setTimeout(() => {
  20. console.log('id:', this.id);
  21. }, 100);
  22. }
  23. var id = 21;
  24. foo.call({ id: 42 });
  25. //输出啥?

Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向

  1. // ES6
  2. function foo() {
  3. setTimeout(() => {
  4. console.log('id:', this.id);
  5. }, 100);
  6. }
  7. // ES5
  8. function foo() {
  9. var _this = this;
  10. setTimeout(function () {
  11. console.log('id:', _this.id);
  12. }, 100);
  13. }
  14. //
  15. //转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this
  16. const cat = {
  17. lives: 9,
  18. jumps: () => {
  19. this.lives--;
  20. }
  21. }
  22. //分析:主要是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域
  23. //相当于
  24. const _this =this
  25. const cat = {
  26. lives: 9,
  27. jumps: () => {
  28. _this.lives--;
  29. }
  30. }

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)

  1. function foo() {
  2. var self = this;
  3. setTimeout(function(){
  4. console.log(self.a);
  5. }, 100);
  6. }
  7. var obj = {
  8. a: 2
  9. };
  10. foo.call(obj);// 2

self=this与箭头函数都可以取代bind,但本质上是替代了this机制
经常编写this风格代码,但绝大部分时候会使用self=this或箭头函数来否定this机制,应当注意以下两点:
a、只是用词法作用域并完全抛弃错误this风格的代码
b、完全采用this风格,在必要时使用bind(),尽量避免使用self=this和箭头函数
两种风格混用通常会使代码更难维护,并且可能也会更难编写