this

34 阅读9分钟

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

1、this 并不是指向函数本身。

2、this 在任何情况下都不指向函数的词法作用域。在JS 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JS 代码访问,它存在于JS 引擎内部。

this 指针

全局作用域下的this

在 JavaScript 中,全局作用域指的是没有包含在任何函数中的代码块。如果在全局作用域中使用 this,它的指向是宿主环境根对象,浏览器中是 Window 对象(在 Node.js 中则是 Global 对象)。

对象方法中的this

对象的函数会进行this 自动绑定,这并不代表函数的内部函数也会自动绑定this(通过箭头函数可以解决this 指向问题)。

构造函数中的this

构造器函数,通过new 关键字调用。

 function Person(name, age) {
     this.name = name;
     this.age = age;
 }
 ​
 let p1 = new Person('Tom', 25);
 console.log(p1); // Person {name: "Tom", age: 25}

从执行上下文看this

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

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用,跟定义无关。

调用位置就在当前正在执行的函数的前一个调用中。

 function baz() {
     // 当前调用栈:baz
     // 因此,当前调用位置是全局作用域
     
     console.log('baz')
     bar() // <-- bar 的调用位置
 }
 ​
 function bar() {
     // 当前调用栈:baz -> bar
     // 因此,当前调用位置在baz 中
     
     console.log('bar')
     foo() // <-- foo 的调用位置
 }
 ​
 function foo() {
     // 当前调用栈:baz -> bar -> foo
     // 因此,当前调用位置在bar 中
     
     console.log('foo')
 }
 ​
 baz() // <-- baz 的调用位置
  • 作为函数被直接调用时,在严格模式下,函数内的this 会被绑定到undefined 上,在非严格模式下则会被绑定到全局对象window/global 上。
  • 作为对象的方法被调用时,函数体内的this 会被绑定到该对象上。
  • 使用new 方法调用构造函数时,构造函数内的this 会被绑定到新创建的对象上。
  • 通过call/apply/bind 方法显示调用函数时,函数体内的this 会被绑定到指定参数的对象上。
  • 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
 const o1 = {
     text: 'o1',
     fn: function() {
         return this.text
     }
 }
 const o2 = {
     text: 'o2',
     fn: function() {
         return o1.fn()
     }
 }
 const o3 = {
     text: 'o3',
     fn: function() {
         var fn = o1.fn
         return fn()
     }
 }
 ​
 console.log(o1.fn()) // o1
 console.log(o2.fn()) // o1
 console.log(o3.fn()) // undefined
 ​
 // 对象的方法
 // 第二个console 中的o2.fn() 最终调用的还是o1.fn(),因此运行结果仍然是o1
 // 第三个console 中的o3.fn() 通过var fn = o1.fn 的赋值进行了“裸奔”调用,因此这里的this 指向window,运行结果是undefined
 ​
 // 如果需要让console.log(o2.fn()) 语句输出o2,不使用bind、call、apply?
 const o2 = {
     text: 'o2',
     fn: o1.fn // 提前进行赋值操作,将函数fn 挂载到o2 对象上,fn 最终作为o2 对象的方法被调用
 }
 console.log(o2.fn())

绑定规则

默认绑定 - window/global/undefined

 // 非严格模式,由于函数调用时前面并未指定任何对象,this 指向全局对象window
 function fn1() {
     let fn2 = function () {
         console.log(this) // window
         fn3()
     }
     console.log(this) // window
     fn2()
 }
 function fn3() {
     console.log(this) // window
 }
 fn1()
 // 在严格模式环境中,默认绑定的this 指向undefined
 ​
 function fn() {
     console.log(this) // window
     console.log(this.name) // 'shaun'
 }
 ​
 function fn1() {
     "use strict"
     console.log(this) // undefined
     console.log(this.name) // TypeError: Cannot read property 'name' of undefined
 }
 ​
 var name = 'shaun'
 ​
 fn()
 fn1()

对于默认绑定来说,决定this 绑定对象的并不是调用位置是否出于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到undefined,否则this 会被绑定到全局对象。

 // 函数以及调用都暴露在严格模式中
 ​
 "use strict"
 var name = 'shaun'
 function fn() {
     console.log(this) // undefined
     console.log(this.name) // 报错
 }
 fn()
 // 如果在严格模式下调用不在严格模式中的函数,并不会影响this 指向
 ​
 var name = 'shaun'
 function fn() {
     console.log(this) //window
     console.log(this.name) //shaun
 }
 ​
 (function () {
     "use strict"
     fn()
 }())

隐式绑定 - 对象属性

 // 对象的属性
 function fn() {
     console.log(this.name)
 }
 let obj = {
     name: 'shaun',
     func: fn
 }
 ​
 obj.func() // shaun
 // 如果函数调用前存在多个对象,this 指向距离调用自己最近的对象
 function fn() {
     console.log(this.name)
 }
 ​
 let obj = {
     name: 'alfred',
     func: fn,
 }
 ​
 let obj1 = {
     name: 'shaun',
     o: obj
 }
 ​
 obj1.o.func() // alfred
 var length = 1
 function fn() {
     console.log(this, this.length)
 }
 var lists = [fn, 11, 22, 33, 44, 55]
 lists[0]()
 ​
 // [ƒ, 11, 22, 33, 44, 55] 6
隐式丢失 - 变量赋值(函数别名)
 function foo() {
     console.log(this.a)
 }
 var obj = {
     a: 2,
     foo: foo
 }
 ​
 var bar = obj.foo // 函数别名!
 var a = 'oops!'
 bar() // 'oops!'
隐式丢失 - 传入回调函数
 var name = '雨'
 let obj = {
     name: '风',
     fn: function () {
         console.log(this.name)
     }
 }
 ​
 //  1 自定义
 function fn1(param) {
     param()
 }
 fn1(obj.fn) // '雨'
 ​
 ​
 // 2 JS 内置函数
 // 如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。严格模式,会被设置为 undefined。
 setTimeout(obj.fn, 100) // ‘雨'

显式绑定 - call/apply/bind

显式绑定是指通过callapply方法改变this 的行为,相比于隐式绑定,能清楚地感知 this 指向变化过程。

如果传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this 的绑定对象,这个原始值就会被转换成它的对象形式(new String()、new Boolean()、new Number())。

硬绑定
 function foo() {
     console.log(this.a)
 }
 var obj = {
     a: 2
 }
 var bar = function() {
     foo.call(obj)
 }
 bar() // 2
 setTimeout(bar, 100) // 2
  
 // 硬绑定的bar 不可能再修改它的this
 bar.call(window) // 2
 ​
 // 创建函数bar(),并在它的内部手动调用foo.call(obj),因此强制把foo 的this 绑定到了obj。
 // 无论之后如何调用函数bar,它总会手动在obj 上调用foo。

创建一个包裹函数,传入所有的参数并返回接收到的所有值:

 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
被忽略的this

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

 let obj1 = {
     name: 'obj1'
 }
 let obj2 = {
     name: 'obj2'
 }
 var name = 'global'
 ​
 function fn() {
     console.log(this.name)
 }
 fn.call(undefined) // global
 fn.apply(null) // global
 fn.bind(undefined)() // global

什么情况会传入null?

 function foo(a, b) {
     console.log('a:' + a + ', b:' + b)
 }
 ​
 // 把数组展开成参数
 foo.apply(null, [2, 3]) // a: 2, b: 3
 ​
 // 使用bind(..) 进行柯里化
 var bar = foo.bind(null, 2)
 bar(3) // a: 2, b: 3

这两种方法都需要传入一个参数当作this 的绑定对象。如果函数并不关心this 的话,仍需要传入一个占位值,这是null 可能是一个不错的选择。

然而,总是使用null 来忽略this 绑定可能产生一些副作用。如果某个函数确实使用了this(如第三方库中的一个函数),那默认绑定规则会把this 绑定到全局对象,这将导致不可预计的后果。更安全的做法是传入一个特殊的对象。

 // DMZ 空对象
 var ø = Object.create(null)
 foo.apply(ø, [2, 3])
call

在指定this 和arguments (参数)调用函数或方法的场景。

call() 方法创建并返回一个新函数,并绑定在传入的对象上。

  • 第一个参数函数体内的this 指向,可不指定(在严格模式下是undefined,默认会绑定为全局对象)
  • 第二个参数,接收任意个参数。
 function sum(num1, num2) {
     return num1 + num2
 }
 ​
 function callSum2(num1, num2) {
     return sum.call(this, num1, num2)
 }
 // 参数对应
 function func(a, b, c) {
     console.log(a, b, c)
 }
 ​
 func.call(null, 1, 2, 3) // 1 2 3
 func.call(null, [1, 2, 3]) // [1, 2, 3] undefined undefined
 // 当调用 greet 方法的时候,该方法的this值会绑定到 obj 对象。
 function greet() {
     var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ')
     console.log(reply)
 }
 ​
 var obj = {
     animal: 'cats',
     sleepDuration: '12 and 16 hours'
 }
 ​
 greet.call(obj)  // cats typically sleep between 12 and 16 hours
使用场景

对象的继承

 function superClass () {
     this.a = 1
     this.print = function () {
         console.log(this.a)
     }
 }
 ​
 function subClass () {
     // 执行函数,this 继承了 superClass 的 print 方法和 a 变量
     superClass.call(this)
     this.print()
 }
 ​
 subClass() // 1

类(伪)数组使用数组方法

 // slice() - 浅拷贝,返回一个新的数组对象
 let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"))
手写call
 Function.prototype.call = function (context = window) {
     if (typeof this !== 'function') {
         return new TypeError('error')
     }
     context.fn = this
     // 将context 后面的参数取出来
     const args = [...arguments].slice(1)
     const res = context.fn(...args)
     delete context.fn
     return res
 }
 ​
 /* 以下是对实现的分析:
 - 首先 context 为可选参数,如果不传的话默认上下文为 window
 - 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
 - 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
 - 然后调用函数并将对象上的函数删除 */
 ​
 ​
 Function.prototype.call2 = function(context, ...args) {
     context = context || window;
     context.__fn = this;
     const res = context.__fn(...args);
     delete context.__fn;
     return res;
 }
 Function.prototype.myCall2 = function(context, ...params) {
     context = context || window;
     // Object 此时充当了一个构造函数的作用,可以理解为类型转换
     !/^(object|function)&/.test( typeof context ) ?
     context = Object(context) : null;
     let _this = this, result = null, UNIQUE_KEY = Symbol('UNIQUE_KEY');
     context[UNIQUE_KEY] = _this;
     res = context[UNIQUE_KEY](...params);
     delete context[UNIQUE_KEY];
     return res;
 }
 ​
 ​
 // TEST CASE
 const a = {
     name: 'shaun'
 };
 function sayName() {
     console.log(this.name);
 }
 ​
 sayName.myCall2(a);
apply

apply() 方法接收两个参数:

  • 第一个参数函数体内的this 指向。如果不传,默认是全局对象window。
  • 一个参数数组或类数组,可以是Array 的实例,也可以是arguments 对象
 function sum(num1, num2) {
     return num1 + num2
 }
 ​
 function callSum1(num1, num2) {
     // this 值等于window,因为是在全局作用域中调用的
     return sum.apply(this, arguments) // 传入arguments 对象
 }
 ​
 function callSum2(num1, num2) {
     return sum.apply(this, [num1, num2]) // 传入数组
 }
 ​
 console.log(callSum1(10, 10)) // 20
 console.log(callSum2(10, 10)) // 20
 function func(a, b, c) {
     console.log(a, b, c)
 }
 ​
 func.apply(null, [1, 2, 3]) // 1 2 3
 func.apply(null, {
     0: 1,
     1: 2,
     2: 3,
     length: 3
 }) // 1 2 3
使用场景

获取数组中的最值

 // 重要的不是 this 的绑定对象,而是 apply 将 array 的数组拆解了作为参数给 Math.max
 let max = Math.max.apply(null, array)
 let min = Math.min.apply(null, array)

数组合并

 let arr1 = [1, 2, 3]
 let arr2 = [4, 5, 6]
     
 Array.prototype.push.apply(arr1, arr2)
 // 这里相当于把 arr2 作为 apply 的第二个参数,把 arr2 拆解了 -- arr1.push(4,5,6)
 // arr1 = [1, 2, 3, 4, 5, 6]
手写apply
 /* 实现分析:
 - 首先 context 为可选参数,如果不传的话默认上下文为 window
 - 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
 - 因为 apply 可以传入数组作为调用函数的参数,所以需要将参数剥离出来
 - 然后调用函数并将对象上的函数删除 */
 ​
 Function.prototype.apply2 = function(context, args) {
     context = context || window;
     context.__fn = this;
     const res = context.__fn(...args);
     delete context.__fn;
     return res;
 }
 Function.prototype.applyFn = function (targetObject, argsArray) {
     if (typeof argsArray === undefined || argsArray === null) {
         argsArray = []
     }
     if (typeof targetObject === undefined || targetObject === null) {
         targetObject = window
     }
     targetObject = new Object(targetObject)
     
     // const targetFnKey = 'targetFnKey'
     const targetFnKey = Symbol()
     targetObject[targetFnKey] = this
     
     const result = targetObject[targetFnKey](...argsArray)
     delete targetObject[targetFnKey]
     return result
 }
 ​
 // 函数体内的this 指向了调用applyFn 的函数。为了将该函数体内的this 绑定在targetObject 上,采用隐式绑定的方法:
 // targetObject[targetFnKey](...argsArray)
 // 这里存在一个问题:如果targetObject 对象本身就存在targetFnKey 这样的属性,那么在使用applyFn 函数时,原有的targetFnKey 属性值就会被覆盖,之后被删除。
 // 解决方案时使用ES6 Symbol() 来保证键的唯一性,或者使用Math.random() 实现独一无二的键。
 const targetFnKey = Symbol()
call vs apply

Function.prototype.apply 和Function.prototype.call 的作用是一样的,区别在于传入参数的不同:

  • 第一个参数都是指定函数体内this 的指向
  • 第二个参数开始不同,apply 是传入带下标的集合,数组或类数组;call 从第二个开始传入的参数是不固定的,都会传给函数作为参数
  • call 比apply 的性能要好
bind

bind() 方法会创建一个新的函数,其this 值会被绑定到传给bind() 的对象。

 window.color = 'red'
 var o = {
     color: 'blue'
 }
 function sayColor() {
     console.log(this.color)
 }
 let objectSayColor = sayColor.bind(o)
 objectSayColor() // blue
 function func(a, b, c) {
     console.log(a, b, c)
 }
 const func1 = func.bind(null, 'D')
 func1('A', 'B', 'C') // D A B
 func1('B', 'C') // D B C
 // 如果连续 bind() 两次,亦或者是连续 bind() 三次
 const bar = function(){
     console.log(this.x)
 }
 const foo = {
     x: 3
 }
 const sed = {
     x: 4
 }
 const func = bar.bind(foo).bind(sed)
 func() // ? => 3
  
 const fiv = {
     x: 5
 }
 const func = bar.bind(foo).bind(sed).bind(fiv)
 func() // ? => 3
 ​
 // 两次都仍将输出3,而非期待中的 4 和 5。多次 bind() 是无效的。
手写bind
 // 简单版
 Function.prototype.bind2 = function(context, ...args) {
     context = context || window
     let _this = this
     return function(...args2) {
         context.__fn = _this
         const res = context.__fn(...[...args, ...args2])
         delete context.__fn
         return res
     }
 }
 var value = 2;
 var foo = {
   value: 1
 };
 ​
 function bar(name, age) {
   this.habit = 'reading';
   console.log(this.value);
   console.log(name);
   console.log(age);
 }
 ​
 bar.prototype.friend = 'lucius';
 ​
 var bindFoo = bar.bind(foo, 'alfred');
 ​
 var obj = new bindFoo(18);
 ​
 console.log(obj.habit);
 console.log(obj.friend);
 ​
 // bind 的实现需要解决以下4个问题: 
 // 1、this 指向问题
 // 2、传参处理
 // 3、构造函数处理
 // 4、原型的修改
 ​
 Function.prototype.bind2 = function(context) {
   if (typeof this !== 'function') {
     throw new Error('this is not a function');
   }
   var _this = this;
   // var args = Array.prototype.slice.call(arguments, 1)
   // ES6
   var args = [...arguments].slice(1);
 ​
   var fBound = function() {
     // var bindArgs = Array.prototype.slice.call(arguments)
     // ES6
     var bindArgs = [...arguments];
     // this instanceof fBound 生成的结果是fBound 的实例,作为构造函数 fBound 等同于bindFoo
     // this instanceof fBound:obj instanceof fBound 若为true,obj.value => undefined
     return _this.apply(this instanceof fBound ? this : context, [
       ...args, ...bindArgs
     ])
     // 或者写成 var finalArgs = args.concat(bindArgs)
     // return _this.apply(this instanceof fBound ? this : context, finalArgs)
   };
     
   // fBound.prototype = this.prototype;
   // 这种写法不太好,修改fBound 的原型会修改bar 的原型
   // 可以定义一个空函数作为周转 => 采用寄生组合继承
   var fNOP = function() {};
   fNOP.prototype = this.prototype;
   fBound.prototype = new fNOP();
 ​
   return fBound;
 };

call、apply与bind 区别

 // 以下3段代码是等价的
 const target = {}
 fn.call(target, 'arg1', 'arg2')
 ​
 const target = {}
 fn.apply(target, ['arg1', 'arg2'])
 ​
 const target = {}
 fn.bind(target, 'arg1', 'arg2')()
  • call、apply 与bind 都用于改变this 绑定,但call、apply 在改变this 指向的同时还会执行函数,而bind 在改变this 后是返回一个全新的绑定函数,这也是为什么bind 后还加了一对括号 () 的原因。
  • bind 属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply 的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
  • call 与apply 功能完全相同,唯一不同的是call 方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call 的性能要高于apply,因为apply 在执行时还要多一步解析数组。
 // 修改 boundFunction 的 this 指向
 ​
 let obj1 = {
     name: '风'
 }
 let obj2 = {
     name: '雨'
 }
 var name = '雷'
 ​
 function fn() {
     console.log(this.name)
 }
 ​
 fn.call(obj1) // 风
 fn() // 雷
 fn.apply(obj2) // 雨
 fn() // 雷
 ​
 let boundFn = fn.bind(obj1)
 boundFn() // 风
 boundFn.call(obj2) // 风
 boundFn.apply(obj2) // 风
 boundFn.bind(obj2)() // 风
 let obj = {
     name: '听风是风'
 }
 ​
 function fn(age, describe) {
     console.log(`我是${this.name},我的年龄是${age},我非常${describe}!`)
 }
 ​
 fn.call(obj,'26','帅') // 我是听风是风,我的年龄是26,我非常帅
 fn.apply(obj,['26','帅']) // 我是听风是风,我的年龄是26,我非常帅

new 绑定

使用new 操作符会执行以下步骤:

  • 创建一个新对象
  • 将新对象的原型设置为构造函数的prototype 属性(为新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象)
  • 将新对象作为this 对象调用构造函数
  • 如果构造函数返回一个对象,则返回该对象;否则返回这个新对象
 function Person(name, age) {
   this.name = name;
   this.age = age;
 }
 ​
 Person.prototype.sayHello = function() {
   console.log('Hello, my name is ' + this.name + ', and I am ' + this.age + ' years old.');
 };
 ​
 let person = new Person('John', 30);
 // person 能够获取到构造函数中的this 指向的属性与原型上的方法
 person.sayHello(); // 输出 "Hello, my name is John, and I am 30 years old."
 ​
 ​
 // 1、创建一个新对象 person。
 // 2、将 person 的原型设置为 Person.prototype。
 // 3、将构造函数 Person 的 this 对象设置为 person,并执行构造函数内部的代码。
 // 4、因为构造函数 Person 没有返回值,所以返回的是新创建的对象 person。

如果在构造函数中出现了显式return 的情况,可以细分为两种场景:

 // 场景1-1
 function Foo() {
     this.uesr = 'Lucius'
     const o = {}
     return o
 }
 const instance = new Foo()
 console.log(instance.user) // undefined,此时instance 返回的是空对象o
 ​
 // 场景1-2
 var puppet = {
     rules: false
 }
 function Emperor() {
     this.rules = true
     return puppet
 }
 var emperor = new Emperor()
 console.log(emperor) // { rules: false }
 ​
 // puppet 对象最终作为构造函数调用的返回值,而且在构造函数中对函数上下文的操作都是无效的。
 // 场景2
 function Foo() {
     this.user = 'Lucius'
     return 1
 }
 const instance = new Foo()
 console.log(instance.user) // Lucius,instance 此时返回的是目标对象实例this

【总结】

  • 如果构造函数返回的是一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this 将被丢弃
  • 如果返回的是非对象类型(如基本类型),则忽略返回值,返回新创建的对象(this 指向实例)

【实现new】

 function Person() {};
 var person = new Person();
 var person = objectFactory(Person, name, age);
 ​
 function objectFactory() {
     var obj = new Object();
     var Constructor = [].shift.call(arguments); // Constructor --> Person, arguments --> name, age
     obj.__proto__ = Constructor.prototype; // obj --> Constructor
     var ret = Constructor.apply(obj, arguments); // 绑定了this
     return typeof ret === 'object' ? ret : obj
 };
 ​
 function myNew() {
   const obj = {}
   const con = [].shift.call(arguments)
   obj.__proto__ = con.prototype
   const res = con.apply(obj, arguments)
   return res instanceof Object ? res : obj
 }
 ​
 /* 以下是对实现的分析:
 - 创建一个空对象
 - 获取构造函数
 - 设置空对象的原型
 - 绑定 this 并执行构造函数
 - 确保返回值为对象 */
 ​
 ​
 function myNew(constructor, ...args) {
   let obj = Object.create(constructor.prototype);
   let res = constructor.apply(obj, args);
   return res instanceof Object ? res : obj;
 }
 ​
 Object.create() 方法是ES5 中新增的方法,用于创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。被创建的对象会继承另一个对象的原型,在创建新对象时还可以指定一些属性。
 ​
 // 首先通过 Object.create() 方法创建了一个空对象 obj,并将其原型设置为构造函数的 prototype 属性。
 // 然后使用 apply() 方法将构造函数的 this 指向该对象,并传递了参数 args。
 // 最后判断构造函数的返回值是否为一个对象,如果是,则返回该对象,否则返回创建的新对象 obj。
 // 需要注意的是,由于 new 操作符在使用时还会进行一些额外的处理,如设置该对象的 constructor 属性等,因此以上实现方式并不完整。

箭头函数的this

箭头函数的this 指向取决于外层作用域中的this,外层作用域或函数的this 指向谁,箭头函数中的this 便指向谁。

 var obj = {
     name: 'abc',
     fn: () => {
         console.log(this.name)
     }
 }
 obj.name = 'bcd'
 obj.fn()
 // 这里函数执行的时候外层是全局作用域,所以this 指向window,window 对象下没有name 属性,所以是undefined。
 function fn() {
     return () => {
         console.log(this.name)
     }
 }
 let obj1 = {
     name: '风'
 }
 let obj2 = {
     name: '雨'
 }
 let bar = fn.call(obj1)
 bar() // 风
 bar.call(obj2) // 风
 // 由于fn 中的this 绑定到了obj1 上,所以bar (引用箭头函数)中的this 也会绑定到obj1 上,箭头函数的绑定无法被修改。
 fn.call(obj1)() // fn this 指向obj1,箭头函数this 也指向obj1
 fn.call(obj2)() // fn this 指向obj2,箭头函数this 也指向obj2
 ​
 // 箭头函数的this 取决于外层作用域的this,fn 函数执行时this 指向了obj1,所以箭头函数的this 也指向obj1

绑定优先级

如果一个函数调用存在多种绑定方法,this 绑定优先级为:

显式绑定 > 隐式绑定 > 默认绑定

new 绑定 > 隐式绑定 > 默认绑定

 // e.g.1
 let obj = {
     name:'first',
     fn: function () {
         console.log(this.name)
     }
 }
 obj1 = {
     name:'second'
 }
 obj.fn.call(obj1) // second
 ​
 // e.g.2
 function foo(a) {
     console.log(this.a)
 }
 const obj1 = {
     a: 1,
     foo: foo
 }
 const obj2 = {
     a: 2,
     foo: foo
 }
 obj1.foo.call(obj2) // 2
 obj2.foo.call(obj1) // 1
 ​
 // e.g.3
 function foo(a) {
     this.a = a
 }
 const obj1 = {}
 var bar = foo.bind(obj1) // 将bar 函数中的this 绑定为obj1 对象
 bar(2)
 console.log(obj1.a) // 2
 ​
 ​
 // e.g.4
 obj = {
     name: '雨',
     fn: function () {
         this.name = '风'
     }
 }
 let echo = new obj.fn()
 console.log(echo.name) // 风

面试

1、this 指向问题的理解

2、箭头函数的特性

3、手写实现call、apply、bind 函数

4、new 创建一个对象时,做了哪些事情?

 // e.g. 1
 var out = 25;
 var inner = {
   out: 20,
   func: function () {
     var out = 30;
     return this.out;
   }
 };
 console.log((inner.func, inner.func)());
 console.log(inner.func());
 console.log((inner.func)());
 console.log((inner.func = inner.func)());
 ​
 // 25、20、20、25
  • 逗号操作符会返回表达式中的最后一个值,这里为inner.func 对应的函数,注意是函数本身,然后执行该函数,该函数并不是通过对象的方法调用,而是在全局环境下调用,所以this 指向window,打印出来的当然是window 下的out
  • 这个显然是以对象的方法调用,那么this 指向该对象
  • 加了个括号,看起来有点迷惑人,但实际上(inner.func)和inner.func 是完全相等的,所以还是作为对象的方法调用
  • 赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数
 // e.g. 2
 function fn (){ 
   console.log(this) 
 }
 var arr = [fn]
 arr[0]() // 打印出arr数组本身,[f]

函数作为某个对象的方法调用,this 指向该对象,数组显然也是对象,obj['fn']

扩展 - 软绑定

对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj 绑定到this,否则不会修改this。

 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' }
 var obj2 = { name: 'obj2' }
 var 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

【参考资料】

《JS 忍者秘籍》第2版 章节4

《你不知道的JavaScript》上卷 第2部分 章节1、2

JS 五种绑定彻底弄懂this