前端-JS学习笔记-函数的this指向

this的四种绑定:默认绑定,隐式绑定,显式绑定(call/apply/bind),new绑定;四种绑定的优先级:new>bind>call/apply>隐式>默认;箭头函数

1. 为什么需要this

  • 在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语言中的this不太一样

    • 常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中
    • 也就是需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象
    • 但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义
  • 编写一个obj的对象,有this和没有this的区别

    • 没有this

      • 从某些角度来说,开发中如果没有this,很多问题也会有解决方案
      • 但是没有this,会让代码的编写变得非常不方便
      • 比如下列代码,函数中希望使用对象中的变量,通过obj.name,如果将obj换成info,则需要将所有name的调用改为info.name,十分不方便
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var obj = {
      name: 'test',
      running: function(){
      console.log(obj.name +" running")
      },
      eating: function(){
      console.log(obj.name +" eating")
      },
      studying: function(){
      console.log(obj.name +" studying")
      }
      }
    • 有this

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var obj = {
      name: 'test',
      running: function(){
      console.log(this.name+" running")
      },
      eating: function(){
      console.log(this.name+" eating")
      },
      studying: function(){
      console.log(this.name+" studying")
      }
      }

2. this指向什么

  • this在全局作用于下,在浏览器中测试就是指向window

    1
    2
    3
    4
    5
    6
    7
    console.log(this);

    var name = "test";
    //test
    console.log(this.name);
    //test
    console.log(window.name);
  • nodejs中的this指的是 {}

    lib/internal/modules/cjs/loader.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const compiledWrapper = wrapSafe(filename, content, this);

    // this.exports = {}
    setOwnProperty(this, 'exports', {});

    const exports = this.exports;
    const thisValue = exports;

    // result = compiledWrapper.call({},exports, require, module, filename, dirname)
    result = ReflectApply(compiledWrapper, thisValue,[exports, require, module, filename, dirname]);

  • 开发中很少直接在全局作用于下去使用this,通常都是在函数中使用

    • 所有的函数在被调用时,都会创建一个执行上下文
    • 这个上下文中记录着函数的调用栈、AO对象等
    • this也是其中的一条记录
  • 先来看一个让人困惑的问题

    • 定义一个函数,采用三种不同的方式对它进行调用,它产生了三种不同的结果

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function foo(){
      console.log(this);
      }

      // 1. 直接调用这个函数
      foo() // window

      // 2. 创建一个对象,对象中函数指向foo
      var obj = {
      name : 'why',
      foo: foo
      }
      //obj对象
      obj.foo()

      // 3. apply/call调用
      foo.apply("abc") // String{"abc"}对象
  • 这个的案例有什么样的启示

    1. 函数在调用时,JavaScript会默认给this绑定一个值
    2. this的绑定和定义的位置(编写的位置)没有关系
    3. this的绑定和调用方式以及调用的位置有关系
    4. this是在运行时被绑定的
  • 那么this到底是怎么样的绑定规则

    • 绑定一:默认绑定
    • 绑定二:隐式绑定
    • 绑定三:显式绑定
    • 绑定四:new绑定

3. 默认绑定 - 独立函数调用

  • 什么情况下使用默认绑定

    • 独立函数调用

    • 可以理解成函数没有被绑定到某个对象上进行调用

  • 通过几个案例来看一下,常见的默认绑定

3.1. 案例一

  • 普通函数被独立调用
1
2
3
4
function foo1(){
console.log(this);
}
foo1();//window

3.2. 案例二

  • 普通函数被独立调用
1
2
3
4
5
6
7
8
9
10
11
12
function test1(){
console.log(this);
test2();
}
function test2(){
console.log(this);
test3();
}
function test3(){
console.log(this);
}
test1();//window

3.3. 案例三

  • 函数定义在对象中,但是独立调用
1
2
3
4
5
6
7
8
9
10
function foo(func){
func();
}
var obj = {
name: "test",
bar: function(){
console.log(this);
}
}
foo(obj.bar);//window

3.4. 案例四

  • 严格模式下,独立调用的函数中的this指向的是undefined
1
2
3
4
5
6
7
"use strict";

function foo(){
console.log(this);
}
// undefined
foo();

4. 隐式绑定 - 对象发起函数调用

  • 另外一种比较常见的调用方式是通过某个对象进行调用的

    • 也就是它的调用位置,是通过某个对象发起的函数调用
  • 通过几个案例来看一下,常见的隐式绑定

4.1. 案例一

1
2
3
4
5
6
7
8
function foo1(){
console.log(this);
}
var obj = {
name: 'test',
foo: foo1
}
obj.foo(); //obj对象

4.2. 案例二

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

4.3. 案例三

1
2
3
4
5
6
7
8
9
function foo3(){
console.log(this);
}
var obj3 = {
name: 'obj',
foo: foo3
}
var bar = obj3.foo;
bar();//window

5. 显式绑定call、apply、bind

  • 隐式绑定有一个前提条件

    • 必须在调用的对象内部有一个对函数的引用(比如一个属性)
    • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
    • 正是通过这个引用,间接的将this绑定到了这个对象上
  • 如果不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做

  • JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)

    • 它们两个的区别:第一个参数是相同的,后面的参数,apply为数组,`call为参数列表``

      • func.apply(thisArg, [argsArray])
      • func.call(thisArg, arg1, arg2, ...)
    • 这两个函数的第一个参数都要求是一个对象,这个对象的作用就是给this准备的

    • 在调用这个函数时,会将this绑定到这个传入的对象上

  • 因为上面的过程,明确的绑定了this指向的对象,称之为显式绑定

  • 通过call或者apply绑定this对象

    • 显示绑定后,this就会明确的指向绑定的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function foo(){
    console.log(this);
    }

    foo.call(window);//window
    foo.call({name:'test'});//{name:'test'}
    foo.call(123);//Number{123}

    function sum(num1,num2){
    console.log(num1+num2,this);
    }
    sum.call("call",1,2);//3 String{'call}
    sum.apply("apply",[1,2]);//3 String{'apply'}
  • 如果希望一个函数总是显示的绑定到一个对象上

    • 使用bind方法,bind()方法创建一个新的绑定函数(bound function BF)
    • 绑定函数是一个 exotic function object(怪异函数对象,ECMAScript2015中的术语)
    • 在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用

    function.bind(thisArg[,arg1[,arg2[,...]]])

    1
    2
    3
    4
    5
    6
    7
    8
    function foo(){
    console.log(this);
    }
    var obj = {
    name: 'test'
    }
    var bar = foo.bind(obj);
    bar();//obj对象

6. new绑定

  • JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字
  • 使用new关键字来调用函数是,会执行如下的操作:
    1. 创建一个全新的对象
    2. 这个新对象会被执行prototype连接
    3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
    4. 如果函数没有返回其他对象,表达式会返回这个新对象
1
2
3
4
5
6
7
8
9
function Person(name){
// Person {}
console.log(this);
this.name = name;
}

var p = new Person("test");
// Person {name:'test'}
console.log(p);

7. 规则优先级

如果一个函数调用位置应用了多条规则,优先级谁更高呢

new>显式>隐式>默认

7.1. 默认规则最低

  • 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this

7.2. 显式绑定级高于隐式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(){
console.log(this);
}

// 1. 显式高于隐式
var obj = {
name: 'test',
foo: foo
}
//String{'abc'}
obj.foo.apply('abc');
//String{'cba'}
obj.foo.call('cba');

// 2. bind绑定高于隐式
obj.foo = foo.bind('nba')
// String{'nba'}
obj.foo()

7.3. bind高于call/apply

1
2
3
4
5
function foo(){
console.log(this);
}
// String{'abc'}
foo.bind('abc').call('cba');

7.4. new绑定级高于隐式绑定

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this);
}
var obj = {
name: 'test',
foo: foo
}
// new绑定高于隐式
// foo {}
var f = new obj.foo()

7.5. new绑定高于bind

  • new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
  • new绑定可以和bind一起使用,new绑定优先级更高
1
2
3
4
5
6
function foo(){
console.log(this);
}
var bar = foo.bind('abc');
// foo {}
var f = new bar();

8. 内置函数的绑定

  • 有些时候,调用一些JavaScript的内置函数,或者一些第三方库中的内置函数
    • 这些内置函数要求传入另外一个函数
    • 并不会显式的调用这些函数,而且JavaScript内部或者第三方库内部会帮忙执行
    • 这些函数中的this又是如何绑定的
  • setTimeout、数组的forEach、div的点击

8.1. setTimeout

1
2
3
4
setTimeout(function(){
// window
console.log(this);
},1000);

8.2. forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
var names = ['abc','cba','nba'];
var obj = {
name: 'test'
};
/**
* Array<string>.forEach(
* callbackfn: (value: string, index: number, array: string[]) => void,
* thisArg?: any): void
*/
names.forEach(function(){
// 三个都指向obj
console.log(this);
},obj);

8.3. div的onclick

1
2
3
4
5
const divEle = document.querySelector('div');
divEle.onclick=function(){
// div对象
console.log(this);
}

9. 箭头函数

  • 箭头函数 arrow function 是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁

    • 箭头函数不会绑定this、arguments属性
    • 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误);
  • 箭头函数如何编写

    • ():函数的参数
    • {}:函数的执行体

9.1. 编写优化

  • 优化一:如果只有一个参数()可以省略

    1
    2
    var nums = [10,20,30,40]
    nums.forEach(item => {})
  • 优化二:如果函数执行体中只有一行代码, 那么可以省略大括号

    • 并且这行代码的返回值会作为整个函数的返回值

      1
      2
      3
      4
      5
      6
      7
      8
      var nums = [10,20,30,40];
      // nums.forEach(item => console.log(item));
      nums.filter(item => true);

      var result = nums
      .filter(item => item%2==0)
      .map(item => item*100)
      .reduce((preValue,item) => preValue+item);
  • 优化三:如果函数执行体只有返回一个对象, 那么需要给这个对象加上()

    1
    2
    3
    4
    5
    6
    var foo = () => {
    return {
    name: 'test'
    }
    }
    var bar = () => ({name:'test'})

9.2. 补充

箭头函数是没有显式原型的,所以不能作为构造函数,使用new来创建对象

1
2
3
4
5
6
7
8
var foo = () => {
console.log("foo");
}

// undefined
console.log(foo.prototype);
// TypeError: foo is not a constructor
var f = new foo();

10. this规则之外

10.1. 忽略显式绑定

  • 如果在显式绑定中,传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function foo(){
    console.log(this);
    }

    var obj = {
    name: 'test'
    }

    // obj对象
    foo.call(obj);
    // window对象
    foo.call(null);
    // window对象
    foo.call(undefined);

    var bar = foo.bind(null);
    // window对象
    bar();
  • 严格模式下,仍为原类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function foo(){
    console.log(this);
    }

    // abc
    foo.call("abc");
    // null
    foo.call(null);
    // undefined
    foo.call(undefined);

    var bar = foo.bind(null);
    // null
    bar();

10.2. 间接函数引用

  • 创建一个函数的间接引用,这种情况使用默认绑定规则
    • 赋值(obj2.foo = obj1.foo)的结果是foo函数
    • foo函数被直接调用,就是默认绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log(this);
}
var obj1 = {
name: 'obj1',
foo: foo
}
var obj2 = {
name: 'obj2'
}
// obj1对象
obj1.foo();
// window 对象
(obj2.foo = obj1.foo)();

10.3. ES6箭头函数

  • 箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this

  • 来看一个模拟网络请求的案例

    • 使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中

    • 需要拿到obj对象,设置data

    • 但是直接拿到的this是window,需要在外层定义:var _this = this

    • 在setTimeout的回调函数中使用_this就代表了obj对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      var obj = {
      data: [],
      getData: function(){
      var _this = this;
      setTimeout(function(){
      // window
      console.log(this);
      var res = ['abc','cba','nba'];
      _this.data.push(...res)
      },1000);
      }
      }
      obj.getData()
      setTimeout(function(){
      //  ['abc', 'cba', 'nba']
      console.log(obj.data);
      },1000);
  • 从ES6开始,可以使用箭头函数

    • 为什么在setTimeout的回调函数中可以直接使用this

    • 因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      var obj = {
      data: [],
      getData: function(){
      setTimeout(() => {
      // obj对象
      console.log(this);
      var res = ['abc','cba','nba'];
      this.data.push(...res);
      },1000);
      }
      }
      obj.getData()
      setTimeout(function(){
      //  ['abc', 'cba', 'nba']
      console.log(obj.data);
      },1000);
  • 如果getData也是箭头函数,那么setTimeout中的回调函数中的this指向 - window

    • 而不是obj对象,因为obj对象并不形成作用域,只有代码块会形成作用域。函数和全局作用域
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = {
    data: [],
    getData: () => {
    setTimeout(() => {
    console.log(this);
    },1000);
    }
    }
    // window
    obj.getData();

11. 面试题一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var name = "window";
var person = {
name: "person",
sayName: function(){
console.log(this.name);
}
};

function sayName(){
var sss = person.sayName;
// window 独立函数调用
sss();
// person 隐式调用
person.sayName();
// person 隐式调用
(person.sayName)();
// window 间接函数引用
(b=person.sayName)();
}
sayName();

12. 面试题二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var name = "window";
var person1 = {
name: "person1",
foo1: function(){
console.log(this.name);
},
foo2: ()=> console.log(this.name),
foo3: function(){
return function(){
console.log(this.name);
}
},
foo4: function(){
return ()=>{
console.log(this.name);
}
}
}
var person2 = {
name: "person2"
}
// person1 隐式调用
person1.foo1();
// person2 显式大于隐式
person1.foo1.call(person2);

// window
// 箭头函数不绑定this,找上级作用域,上级作用域为全局
person1.foo2();
// window
// 箭头函数的上级作用域为全局
person1.foo2.call(person2);

// window
// person1.foo3()执行后拿到的是return的函数,再()是独立函数调用
person1.foo3()();
// window
// 执行call后this指向person2,再()是独立函数调用
person1.foo3.call(person2)();
// person2
//person1.foo3()拿到return后的函数,再进行的显式调用call
person1.foo3().call(person2);

// person1
// person1.foo4()拿到return后的箭头函数,箭头函数的上级作用域是一个函数,this指向person1
person1.foo4()();
// person2
// person1.foo4.call(person2)箭头函数的上级this绑定到person2,再()就找到person2
person1.foo4.call(person2)();
// person1
// person1.foo4()拿到箭头函数,箭头函数的上级this指向person1,call不能显式绑定箭头函数,仍指向person1
person1.foo4().call(person2);

13. 面试题三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var name = "window";
function Person(name){
this.name = name;
this.foo1 = function(){
console.log(this.name);
};
this.foo2 = ()=> console.log(this.name);
this.foo3 = function(){
return function(){
console.log(this.name);
}
};
this.foo4 = function(){
return ()=>{
console.log(this.name);
}
}
}

var person1 = new Person("person1");
var person2 = new Person("person2");

// person1 隐式绑定
person1.foo1();
// person2 显式高于隐式
person1.foo1.call(person2);

// person1 箭头函数看上级作用域person1
person1.foo2();
// person1
// call在箭头函数中不起作用,看箭头函数上级作用域person1
person1.foo2.call(person2);

// window
// person1.foo3()拿到return的函数,再()是独立函数调用
person1.foo3()();
// window
// person1.foo3.call(person2)得到函数,再()是独立函数调用
person1.foo3.call(person2)();
// person2
// person1.foo3()拿到return的函数,通过call显式绑定,this指向person2
person1.foo3().call(person2);

// person1
// person1.foo4()拿到箭头函数,再()箭头函数上级作用域为person1
person1.foo4()();
// person2
// 箭头函数的上级显式绑定到person2,再()就找到的是person2
person1.foo4.call(person2)();
// person1
// person1.foo4()拿到的是箭头函数,call不起作用,箭头函数上级作用域person1
person1.foo4().call(person2);

14. 面试题四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var name = "window";
function Person(name){
this.name = name;
this.obj = {
name: "obj",
foo1: function(){
return function(){
console.log(this.name);
}
},
foo2: function(){
return ()=>{
console.log(this.name);
}
}
}
}

var person1 = new Person("person1");
var person2 = new Person("person2");

// window
// person1.obj.foo1()拿到return的函数,再()就是独立函数调用
person1.obj.foo1()();
// window
// 显式绑定后返回的函数,再()还是独立函数调用
person1.obj.foo1.call(person2)();
// person2
// person1.obj.foo1()拿到return的函数,进行显式绑定,this指向了person2
person1.obj.foo1().call(person2);

// obj
// 箭头函数上级作用域为obj
person1.obj.foo2();
// person2
// 箭头函数上级作用域显示绑定到person2
person1.obj.foo2.call(person2)();
// obj
// person1.obj.foo2()拿到箭头函数,call不起作用,箭头函数上级作用域为obj
person1.obj.foo2().call(person2);

15. 面试题五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var x = 0;

function foo(
x,
y = function () {
x = 3;
console.log(x);
}
) {
console.log(x);
var x = 2;
y();
console.log(x);
}

foo();

console.log(x);

/**
undefined
3
2
0
*/

参数作用域:https://262.ecma-international.org/#sec-functiondeclarationinstantiation

If the function’s formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations

当函数得到参数有默认值时,会形成一个新的作用域,这个作用域用于保存参数的值

16. 实现apply、call、bind

16.1. call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 给所有函数添加一个hycall方法
Function.prototype.hycall = function(thisArg, ...args){
// 1.获取需要被执行的函数
var fn = this;

// 2.对thisArg转成对象类型(防止它传入的是非对象类型)
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
else{
thisArg = Object(thisArg);
}

// 3.调用需要被执行的函数
thisArg.fn = fn;
var result = thisArg.fn(...args);

delete thisArg.fn;

// 4.将最终的结果返回出去
return result;
}

function foo(){
console.log(this);
}

// 调用自己实现的hycall方法
// 实际为隐式调用
foo.hycall({name:'test'}); //{}
foo.hycall(1); //Number
foo.hycall(null); //window
foo.hycall(undefined) //window

16.2. apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 给所有函数添加一个hyapply方法
Function.prototype.hyapply = function(thisArg, args){
// 1.获取需要被执行的函数
var fn = this;

// 2.对thisArg转成对象类型(防止它传入的是非对象类型)
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
else{
thisArg = Object(thisArg);
}

// 3.调用需要被执行的函数
thisArg.fn = fn;

args = args || [];
result = thisArg.fn(...args);

delete thisArg.fn;

// 4.将最终的结果返回出去
return result;
}

function sum(num1, num2){
console.log(num1 + num2, this);
}

// 调用自己实现的hyapply方法
// 实际为隐式调用
sum.hyapply({name:'test'},[1,2]);
sum.hyapply("abc");

16.3. bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 给所有函数天机一个hybind方法
Function.prototype.hybind = function(thisArg, ...bindArgs){
// 1.获取需要被执行的函数
var fn = this;

// 2.对thisArg转成对象类型(防止它传入的是非对象类型)
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
else{
thisArg = Object(thisArg);
}

// 3.调用需要被执行的函数
thisArg.fn = fn;

return function(...newArg){
var args = [...bindArgs, ...newArg];
var result = thisArg.fn(...args);
delete thisArg.fn;
return result;
}
}

function sum(num1, num2, num3){
console.log(num1 + num2 + num3, this);
}

// 调用自己实现的hybind方法
// 实际为隐式调用
var result = sum.hybind({name:'test'},[1,2]);
result(3);
本文结束  感谢您的阅读