(五)this全面解析

this的误解

this指向函数自身

大多数人会根据this的字面意思将它理解为【指向函数自身】。

什么情况下,this会指向自身呢?

  1. 递归 。也就是从函数本身的内部调用自己
  2. 当我们写一个第一次调用后,自己解除绑定的事件处理器时,这个时候也需要使用到函数自身

但是this真的就是指向函数自身吗?

function foo(num){
  console.log("foo:" + num)
  // 记录foo被调用的次数
  this.count++
}
foo.count = 0
var i
for (i = 0; i < 10; i++) {
  if (i > 5) {
    foo(i)
  }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次
console.log(foo.count) // 0 天,竟然是0

上面的this实际指得是 window全局对象,而不是函数自身。
【如何让函数可以真的指向它自身呢?】
单单靠this是不够的,一般的我们还需要通过一个指向函数对象的词法标识符(变量)来引用它
【栗子一】

function foo () {
  foo.count = 4; 
}

这里的foo是指向它自身
【栗子二】

setTimeout(function(){
 // 匿名(没有名字)函数无法指向自身
},100)

如上所示,第一个例子函数被称为具名函数,因此可以使用它的函数名来表示它自身;但是第二个例子中的函数是匿名函数(没有标识符),因此无法从函数内部引用自身

还有一种传统的但是已经被弃用和批判的用法,即使用arguments.callee来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。但是最好的方式还是避免使用匿名函数,至少在需要引用自身方法时尽量使用具名函数(表达式)而不使用匿名函数

因此对于具名函数我们可以使用函数名来代表当前函数自身
针对上面的例子可以更改为:

function foo(num){
  console.log("foo:" + num)
  // 记录foo被调用的次数
 foo.count++
}
foo.count = 0
var i
for (i = 0; i < 10; i++) {
  if (i > 5) {
    foo(i)
  }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次
console.log(foo.count) // 4 ,这次是4啦

这种方法完全是依赖于foo的词法作用域,还有一种方法是确实可以让this指向函数自身(准确的说是可以任意指定this的指向)

function foo(num){
  console.log("foo:" + num)
  // 记录foo被调用的次数。这里的this确实是foo啦
 this.count++
}
foo.count = 0
var i
for (i = 0; i < 10; i++) {
  if (i > 5) {
  // 注意这里的foo是如何被调用的
    foo.call(foo, i)
  }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次
console.log(foo.count) // 4 ,这次也是4啦

这次的this确实是指向函数自身啦

this指向函数作用域

咳咳,需要知道的是:在js内部,作用域确实和对象类似,且可见的标识符都是它的属性,但是作用域“对象”是无法通过js代码访问的,它存在于js引擎内部
因此this在任何情况下都不指向函数的词法作用域

this到底是什么

【要知道的】
当一个函数被调用时,会创建一个记录(有时候也称为执行上下文)。这个记录包含了函数在哪里被调用(调用栈),函数的调用方式,传入的参数信息等。而this就是这个记录的一个属性

因此this实际上是在函数被调用时发生的绑定,他指向什么完全取决于函数在哪里被调用。而和函数声明的位置没有关系

this的全面解析

因为this是在函数被调用时绑定的,要想知道this就必须先寻找函数的调用位置 =》然后在根据this的绑定规则的优先级来确定它的绑定对象

默认绑定

即独立函数调用。不带任何修饰函数引用进行调用
foo运行在非严格模式下,此时的this指向widow.(在严格模式下调用foo则不影响默认绑定)
foo运行在严格模式下,this则不能将全局对象用于默认绑定,因此this会绑定到undefined上
【foo运行在严格模式下】

function foo(){
  "use strict"
  console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined

【在严格模式下调用foo】

function foo(){
  "use strict"
  console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined

【foo运行在非严格模式下】

function foo(){
  console.log(this.a)
}
var a = 2
(function(){
  "use strict"
  foo(); // 2
})()

隐式绑定

看调用位置是否有上下文对象,或者说是否被某个上下文对象拥有或者包含。如果有,隐式绑定规则会把函数中的this绑定到这个上下文对象

function foo () {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo
}
obj.foo // 2

要知道的:foo函数虽然被当做引用作为obj的属性,但是严格来说它不属于obj对象

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会都丢失绑定对象,也就是说它会使用默认绑定,从而把this绑定到全局对象上火undefined上(取决于函数是否运行在严格模式下)

function foo () {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo 
}
var bar = obj.foo
var a = "opps global" // 全局对象上的属性
bar() // oops global

虽然bar也是obj.foo的一个引用,但是它实际上引用的是foo函数本身,因此bar()其实是一个不带任何修饰符的函数调用,因此使用了默认绑定
类似的情况也会发生在回调函数中:

function () {
  console.log(this.a)
}
function doFoo (fn) {
  fn()
}
var obj = {
  a: 2,
  foo: foo
}
var a = "opps global"
doFoo(obj.foo) // opps global

这里的参数传递实际上就是一种隐式赋值
如果把函数传递给语言内置的函数,而不是自己定义的函数函数内呢?还会发生隐式丢失吗?

function foo () {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo
}
var a = "oops global"
setTimeout(foo, 100) // oops global

实际上js环境内置的setTimeout()函数实现和下面的伪代码类似

function setTimeout (fn, delay) {
  // 等待delay秒后
  fn() // 调用位置
}

显示绑定

【隐式绑定特点】:

必须在一个对象内部包含一个指向函数的属性,并通过这个属性来间接引用函数,从而才可以把this间接(隐式)的绑定到这个对象上

那么如果我们不想在对象内部包含函数引用,而是直接想在某个对象上强制调用函数。(想把函数中的this绑定到任意的对象上)

call、apply

function foo(){
  console.log(this.a)
}
var obj = {
  a: 2
}
foo.call(obj) // 2

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

硬绑定

硬绑定是显示绑定的一个变种。硬绑定的典型应用场景:创建一个包裹函数,这个函数主要负责接受参数并返回值
【不接受参数,也不返回值的包裹函数】

function foo () {
  console.log(this.a)
}
var obj = {
  a: 2
}
// 创建一个包裹函数
var bar = function () {
  foo.call(obj) // 将foo中的this强制绑定到obj对象上
}
bar() // 2 foo里面的this是obj
setTimeout(bar, 100) // 2  foo里面的this没有隐式丢失
bar.call(window) // 2 硬绑定的bar也不能修改foo里面的this

【接受参数并返回返回数据的包裹函数】

function foo(something){
  console.log(this.a, something)
  return this.a + something
}
var obj = {
  a: 2
}
// 创建一个包裹函数
var bar = function () {
  // 向foo传入参数并返回foo执行的结果
  return foo.apply(obj, arguments)
}
var result = bar(3); // 2 3
console.log(result) // 5

另外一个种就是创建一个可以重复使用的辅助函数

function foo (something) {
  console.log(this.a, something)
  return this.a + something
}
// 简单的辅助绑定函数
function bind (fn, obj) {
  return function(){
    fn.apply(obj, arguments)
  }
}
var obj = {
  a: 2
}
var bar = bind(foo, obj)
var result = bar(3) // 2 3
console.log(result) // 5

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

  function foo (something) {
    console.log(this.a, something)
  }
  var obj = {
    a: 2
  }
  var bar = food.bind(obj)
  var b = bar(3) // 2, 3
  console.log(b)

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

API调用的"上下文"

其实在第三方库的许多函数,以及js语言和宿主环境中许多的内置函数,都提供了一个可选的参数,通常被称为“上下文”,它的作用和bind(...)一样,确保你的回调函数使用指定的this

function foo(el){
  console.log(el, this.id)
}
var obj = {
  id: "awesome"
}
// 调用foo(...)把this绑定到obj上
[1, 2, 3].forEach(foo, obj) // 1 awesome 2 awesome 3 awesome

上面的原始实际上也是通过call(...)apply(...)实现了显示绑定,这样我们也可以写些代码

new绑定

【要知道的:】js中的new的机制和传统面向类的语言中的完全不同

js中的构造函数

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

实际上在js中并不存在所谓的“构造函数”,只有对函数的“构造调用”

【使用new来调用函数,或者说当函数发生构造调用时】会自动执行下面的操作:

  1. 创建或构造一个全新的对象
  2. 这个新对象会被执行[[prototype]]连接
  3. 这个新对象会被绑定到函数调用的this上
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
  this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2
// 因此这里的 bar.a 实际上和foo里面的 this.a 是同一个对象上的数据

使用new来调用foo(…)时,我们会构造一个新的对象并把它绑定到foo(…)调用中的this上,最后如果没有返回指定对象,则将刚在新创建的对象返回

function foo (a) {
  var this = {}
  ...
  return this
}

四种绑定的优先级

在这里插入图片描述

显示 > 隐式
new > 隐式

【那么 new和显示的优先级呢?】
new 和 call/apply无法一起使用,因此无法通过new foo.call(obj1)来直接进行测试。但是我们可以使用硬绑定来测试他俩的优先级。

因为callapply都是调用函数,并改变函数中的this,但是仅仅只是改变了this,执行完callapply之后并没有返回一个函数类型的数据。而new是对【函数】的构造调用,因此不可以new一个非函数的东东的。

js中提供的硬绑定即Function.prototype.bind(...)的工作原理:会创建一个新的包装函数,而且这个函数也会忽略它当前的this绑定(无论绑定的对象是什么)都会把我们提供的对象绑定到this上

这样看起来硬绑定(也是硬绑定的一种)似乎比new绑定的优先级更高,无法shiyongnew来控制this绑定

function foo(something){
  this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
var baz = new bar(3)
console.log(obj1.a)
console.log(baz.a)

在这里插入图片描述

哈哈,虽然硬绑定很强硬把this固定住不许其他人更改,但是new很聪明,人家使用自己创建了的this,不使用硬绑定的this。因此new > 硬绑定
总上所述:
new > 显示绑定 > 隐式绑定 > 默认绑定

如何判断this

  1. 函数是否在new中调用(new绑定)?如果是,this就是新创建的对象
var bar = new foo() // foo里面的this就是bar
  1. 函数是否通过call、apply(显式绑定)或应绑定调用(bind)? 如果是,this就是被绑定的那个指定对象
 var bar = foo.call(obj) // foo里面的this就是obj
  1. 函数是否在某个上下文对象中调用(隐式绑定),如果是,this就是那个上下文对象
 var bar = obj1.foo() // foo里面的this就是obj1
  1. 如果都不是,就是用默认绑定。如果是严格模式下,this就绑定到undefined上,否则就绑定到全局对象上
 var bar = foo() // 此时foo里面的this就是undefined或全局对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值