[nodejs 内功心法] this全面解析

转载请注明出处

this

在理解 this 的绑定过程之前,首先要理解调用位置: 调用位置就是函数在代码中被调用位置(而不是声明的位置)。

调用栈和调用位置

	function foo() {
		// 当前调用栈是:foo
		// 当前调用位置是全局作用域
		console.log("foo")
		bar() // bar的调用位置
	}
	function bar() {
		// 当前调用栈是 foo -> bar
		// 当前调用位置在foo中
		console.log("bar")
	}

注意我们是如何分析出函数真正的调用位置,因为它决定了this的绑定

this的绑定规则

1.默认绑定

独立函数调用时会使用默认绑定, 默认绑定this会指向全局对象或undefined(取决于是否是严格模式).
当函数运行在非严格模式时,默认绑定才能绑定到全局对象。严格模式下this会绑定到undefined

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

varible = 2
foo() // 2

上面这个例子 this 指向了全局变量,那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo() 是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

function foo() {
  'use strict'
  console.log(this.varible)
}

varible = 2
foo() 
// TypeError: Cannot read property 'varible' of undefined
function foo() {
	console.log(this.varible)
}
varible = 2;

(function(){
	'use strict'
	foo()
})() // 2

//这里就说明了只有在函数运行在严格模式下默认绑定才会绑定undefinded, 
//而不是函数的调用位置是严格模式。
2.隐式绑定

这条规则主要是考虑调用位置是否有上下文对象

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

obj = { a: 100, foo: foo }

obj.foo() // 100

上面的代码中当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。

对象属性引用链中只有最后一层会影响this的绑定(或者说离函数调用最近的那个对象会影响)

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

obj2 = {
	a: 200,
	foo: foo
}

obj1 = {
	a: 100,
	obj2: obj2
}

obj1.obj2.foo() //200

隐式丢失

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

function demo() {
	console.log(this.a)
}

const obj = {
	a: 100,
	demo: demo
}

const bar = obj.demo
a = 200
bar() // 200

// 此时的 bar() 其实是一个不带任何修饰的函数调用,
// 因此应用了默认绑定。
function demo() {
	console.log(this.a)
}

const obj = {
	a: 1,
	demo: demo
}

a = "i am global"

// 在浏览器中执行
setTimeout(obj.demo, 100) // i am global

// 在node.js环境中执行
setTimeout(obj.demo, 100) // undefined
// 这是因为node.js 中 _onTimeout = obj.demo 是被timer._onTimeout()调用。
// 所以this为timer对象
3.显示绑定

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

call 和 apply

它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为可以直接指定 this 的绑定对象,因此我 们称之为显式绑定。

function demo() {
	console.log(this.a)
}
const obj = { a: 1 }

demo.call(obj) // 1

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

call 和 apply的区别:

它们的区别只是参数的区别。

  1. apply的第二个参数是数组,会结构成参数列表传给方法。Function.apply(obj, args)
  2. call可以传入一个参数列表 Function.call(obj,[params1[,params2]])
硬绑定

es5提供了内置的方法Function.prototype.bind, bind(…) 会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

function demo(str) {
	console.log(this.a, str)
}

const obj = { a: 2 }

const bar = demo.bind(obj);

bar("heihei"); // 2 heihei

forEach的第二个参数就可以指定this上下文(context)

function demo(el) {
	console.log(el, this.id);
}
const obj = { id: 'test!!' };

[1,2,3].forEach(demo, obj)

// 1 'test!!'
// 2 'test!!'
// 3 'test!! 

这些函数实际上就是通过 call(…) 或者 apply(…) 实现了显式绑定

4. new 绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。通常的形式是这样的:

     something = new MyClass(..);

JavaScript 也有一个 new 操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为 JavaScript 中 new 的机制也和那些语言一样。然而,JavaScript 中 new 的机制实际上和面向类的语言完全不同。

在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。(这一部分比较重要,大家要重点学习一下)

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

const obj = new demo(2)
console.log(obj) // demo { a: 2 }

// 使用 new 来调用 demo(..) 时,我们会构造一个新对象并把它绑定到 demo(..) 调用中的 this 上。

new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

优先级

这四种规则的优先级

1.隐式和显示

function demo() {
	console.log(this.a)
}

const obj1 = { a: 1, demo: demo }
const obj2 = { a: 2, demo: demo }

obj1.demo() // 1
obj2.demo() // 2

obj1.demo.call(obj2) // 2
obj2.demo.call(obj1) // 1

可以看到显示绑定的优先级更高

  1. new 和 隐式绑定
function demo(str) {
	this.a = str
}

const obj1 = { demo: demo }

obj1.demo(2);
console.log(obj1.a) // 2

obj1.demo.call(obj2={}, 3)
console.log(obj2.a) // 3

const obj3 = new obj1.demo(4)
console.log(obj1.a) // 2
console.log(obj3.a) // 4

const obj5 = obj1.demo.bind(obj4=Object.create(null))
obj5(5)
console.log(obj4.a) //5

const obj6 = new obj5(6)
console.log(obj4.a) // 5
console.log(obj6.a) // 6

可以看到new绑定的优先级比隐式绑定和bind硬绑定都要高, 这是因为es5的bind方法的实现会判断硬绑定函数是否是被 new 调用,如果是的话就会使用新创建 的 this 替换硬绑定的 this。(new绑定是否优先级高这是根据bind的实现决定的)

为什么要在 new 中使用硬绑定函数呢? 直接使用普通函数不是更简单吗?

之所以要在 new 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用 new 进行初始化时就可以只传入其余的参数。bind(…) 的功能之一就是可以把除了第一个 参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部 分应用”,是“柯里化”的一种)

function demo(arg1, arg2) {
	this.a = arg1 + arg2
}

const bar = demo.bind(null, "debug: ")
const foo = new bar("i am jim")

console.log(foo.a) // debug: i am jim

总结一下上面的内容

this 绑定优先级顺序

  1. 函数是否在new中调用(new绑定)? 如果是的话this绑定的是新创建的对象 (ex: const bar = new foo())
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用? 如果是的话,this绑定的是指定的对象。(ex: bar = foo.call(obj2))
  3. 函数是否在某个上下文对象中调用(隐式绑定)? 如果是的话,this 绑定的是那个上下文对象。(ex: bar = obj.foo())
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象(window 或者 global)。(ex: var bar = foo())

对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。不过还有一些例外的情况

1.如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则(即被绑定到全局对象(window 或者 global))
// 运行在非严格模式下,如果在严格模式下会报错,因为严格模式this是默认绑定undefined
function demo() {
	this.a = 100
}

demo.call(null)
// run in node.js
console.log(global.a) // 100

// run in browser
console.log(window.a) // 100	

使用null 和 undefined作为this绑定的情况

一种非常常见的做法是使用 apply(…) 来“展开”一个数组,并当作参数传入一个函数。类似地, bind(…) 可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用

function test(a, b) {
	console.log("a:" + a + ", b:" + b)
}

// 把数组展开成参数
test.apply(null, [2,3])
// 在 ES6 中,可以用 ... 操作符代替 apply(..) 来“展
// 开”数组, foo(...[1,2]) 和 foo(1,2) 是一样的,这样可以避免不必要的
// this 绑定。可惜,在 ES6 中没有柯里化的相关语法,因此还是需要使用
// bind(..) 。

// 使用bind来进行柯里化
const curring = test.bind(null, 2)
curring(5) // a:2, b:5

然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了this (比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览器中这个对象是 window ),这将导致不可预计的后果(比如修改全局对象)。显而易见,这种方式可能会导致许多难以分析和追踪的 bug。

更安全的this

一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序产生任何副作用。我们可以创建一个“DMZ”(demilitarized
zone,非军事区)对象, 它就是一个空的对象。
如果我们在忽略 this 绑定时总是传入一个 DMZ 对象,那就什么都不用担心了,因为任何对于 this 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)
Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object.prototype 这个委托,所以它比 {} “更空”

function test(a,b) {
	console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );

// 把数组展开成参数
test.apply( ø, [2, 3] ); // a:2, b:3

// 使用 bind(..) 进行柯里化
var bar = test.bind( ø, 2 );
bar( 3 ); // a:2, b:3
间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

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

var a = 1
var foo = { a: 3, test }
var bar = { a: 4 }
foo.test() // 3
(bar.test = foo.test)(); // 1

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是p.foo() 或者 o.foo() 。根据我们之前说过的,这里会应用默认绑定。

注意:

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

this词法 (针对箭头函数() => {})

我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this 。

function test() {
	return () => {
		// this 继承自test
		console.log(this.a)
	}
}

const obj1 = { a: 1 }
const obj2 = { a: 2 }

const foo = test.call(obj1)
foo.call(obj2) // 1 
//并不是返回 2

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

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() {
  setTimeout(() => {
    // 这里的 this 在此法上继承自 foo()
    console.log( this.a );
  },100);
}

var obj = {
  a:2
};

foo.call( obj ); // 2

如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this 或者箭头函数来否定 this 机制,那你或许应当:

  1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(…) ,尽量避免使用 self = this 和箭头函数。

当然,包含这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混合使用这两种风格通常会使代码更难维护,并且可能也会更难编写。

小结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null) ,以保护全局对象。

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样

排版可能有点乱,还需要多多改进,看官们多包涵,哈哈!

转载请注明出处哟:https://blog.csdn.net/a675697174/article/details/103689501

发布了35 篇原创文章 · 获赞 2 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/a675697174/article/details/103689501