Skip to content

JavaScript 深入理解this

this指向

this 不是静态的,this 在运行时才绑定,它的绑定和函数声明的位置没有关系,只取决于函数调用的方式

  • 非严格模式下:this 不能是 undefined 或 null。当 this 指向为 undefined 或 null 时,this 会指向全局对象
  • 严格模式下:this 会区分指向,正常指向 undefined 和 null

this 指向有一个优先级链,后者不能更改前者的 this 指向

  • 箭头函数 > new关键字 > bind、apply、call > obj. >直接调用

image-20240606151702814

改变this指向

call( )

call()

  • 立即调用一个函数,并指定函数内部的 this
  • 如果call()传入的this上下文是undefinednull,那么window对象将成为默认的this上下文
js
fun.call(thisArg, arg1, arg2, ...)
//thisArg:在 fun 函数运行时指定的 this 值
//arg1,arg2:传递的其他参数

常见用法:

  • 使用call实现属性继承(继承中常用)
  • Object.prototype.toString.call(数据) 检测数据类型
js
//检查数据类型(重写)
function checkType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1)
}

apply( )

apply()

  • 立即调用一个函数,并指定函数内部的 this
js
fun.apply(thisArg, [argsArray])
//thisArg:在fun函数运行时指定的 this 值
//argsArray:传递的值,必须包含在数组里面

常见用法:

  • 主要用于参数为数组时
js
//es6之前可以用于求数组的最大值
const arr = [1, 6, 12, 2, 33]
console.log(Math.max.apply(null, arr))

bind( )

bind()

  • bind() 方法不会调用函数,但是能改变函数内部this
  • bind()返回一个新的函数,该函数是改变this后的原函数的拷贝
js
fun.bind(thisArg, arg1, arg2, ...)
//thisArg:在 fun 函数运行时指定的 this 值
//arg1,arg2:传递的其他参数

常见用法:

  • 只改变 this 指向,不调用函数时,可以使用 bind。比如改变定时器内部的 this 指向
js
const obj = {
  message: 'Hello',
  greet: function () {
    console.log(this.message)
  }
}

// 使用 bind 修改 setTimeout 内部回调函数的 this 指向
setTimeout(obj.greet, 1000) //undefined
setTimeout(obj.greet.bind(obj), 1000) //Hello

总结

相同点方法传递参数是否调用函数使用场景
改变this指向call参数列表arg1, arg2...调用函数Object.prototype.toString.call()检测数据类型
改变this指向apply数组、伪数组调用函数跟数组相关,比如求数组最大值和最小值等
改变this指向bind参数列表arg1,arg2...不调用函数改变定时器内部的this指向

this指向面试题

基础部分

  • let、const 变量不会挂载到 window(不能被window访问到,但可以全局直接访问)
js
let a = 1;
const b = 2;
var c = 3;
function print () {
  console.log(this.a); //undefined
  console.log(this.b); //undefined
  console.log(this.c); //3
}
print();
  • 当函数不作为对象的方法调用时this 会指向全局对象
js
a = 1;
function foo () {
  console.log(this.a);
}
const obj = {
  a: 10,
  bar () {
    foo(); // 1,this指向window
  }
}
obj.bar();
  • 事件回调函数中,this指向绑定的DOM元素;不能用箭头函数,否则会指向window
js
document.getElementById('btn').addEventListener('click', function() {
  console.log(this); // <button id="btn"></button>
})
  • 立即执行函数中,this 指向 window
js
a = 1;
(function () {
  console.log(this); //window
  console.log(this.a) //1
}())
function bar () {
  b = 2; // 不写标识符 默认为全局变量
  (function () {
    console.log(this); //window
    console.log(this.b) //2
  }())
}
bar();
  • 对象链式调用,this 绑定最近的对象
js
var obj1 = {
  a: 1,
  obj2: {
    a: 2,
    foo () {
      console.log(this.a)
    }
  }
}
obj1.obj2.foo() // 2
  • 浏览器环境中,没有明确指定调用对象时,this 默认指向全局对象,通常是 window。此处的foo()不是window.foo()
js
a = 1
var obj = {
  a: 2,
  foo() {
    console.log(this.a)
  },
}
let foo = obj.foo
obj.foo() // 2
foo() // 1
  • obj2.foo指向了obj.foo的堆内存,此后执行与obj无关
js
var obj = {
  a: 1,
  foo () {
    console.log(this.a)
  }
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }

obj.foo(); //1
foo(); //2
obj2.foo(); //3
  • call改变this指向,注意返回函数中的this,万变不离其宗
js
var obj = {
  a: 'obj',
  foo: function () {
    console.log('foo:', this.a)
    return function () {
      console.log('inner:', this.a)
    }
  }
}
var a = 'window'
var obj2 = { a: 'obj2' }

obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)

//输出结果:
// foo: obj
// inner: window
// foo: obj2
// inner: window
// foo: obj
// inner: obj2
  • 使用call、apply、bind链式调用,以第一次为准
js
obj = {
  _name: "obj",
  func() {
    const arrowFunc = () => {
      console.log(this._name)
    }
    return arrowFunc
  }
}

obj.func()() //obj
func = obj.func
func()() //undefined, 直接调用, this指向window
obj.func.bind({ _name: "newObj" })()() //newObj
obj.func.bind()()() //undefined, 此处this指向window
obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })() //bindObj
  • 箭头函数没有 自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时
js
name = 'tom'
const obj = {
  name: 'zc',
  intro: function () {
    return () => {
      console.log('My name is ' + this.name)
    }
  },
  intro2: function () {
    return function () {
      console.log('My name is ' + this.name)
    }
  }
}
obj.intro2()() //My name is tom
obj.intro()() //My name is zc
  • 箭头函数没有this,不能通过call/apply/bind来修改this指向,但可以通过修改外层作用域this来达成间接修改
js
var name = 'window'
var obj1 = {
  name: 'obj1',
  intro: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  intro2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.intro.call(obj2)() //obj2 obj2
obj1.intro().call(obj2) //obj1 obj1
obj1.intro2.call(obj2)() //window window
obj1.intro2().call(obj2) //window obj2
  • 本题为上文全部类型的集合
js
var name = 'window'
var user1 = {
  name: 'user1',
  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 user2 = { name: 'user2' }

user1.foo1() //user1
user1.foo1.call(user2) //user2

user1.foo2() //window
user1.foo2.call(user2) //window

user1.foo3()() //window
user1.foo3.call(user2)() //window
user1.foo3().call(user2) //user2

user1.foo4()() //user1
user1.foo4.call(user2)() //user2
user1.foo4().call(user2) //user1

综合提升

  • (foo.bar = foo.bar)()意为赋值并调用,类似于取别名;(foo.bar, foo.bar)逗号表达式,前后执行完后返回右边表达式的结果
js
var x = 10;
var foo = {
  x: 20,
  bar: function () {
    var x = 30;
    console.log(this.x)
  }
};
foo.bar(); //20
(foo.bar)(); //20
(foo.bar = foo.bar)(); //10,这里相当于是一个新的函数,在全局作用域内执行
(foo.bar, foo.bar)(); //10,这里相当于是一个新的函数,在全局作用域内执行
  • arguments为伪数组,实际上是一个对象,不过对象属性有0、1、2编号,并且有length属性
js
var length = 10;
function fn () {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function (fn) {
    fn();
    arguments[0](); //这里相当于arguments.fn()
  }
};

obj.method(fn, 1); //10 2
  • JavaScript 作用域链为静态,在函数定义时就已经确定;对象中的立即执行函数,会在对象创建时自动执行
js
var number = 5;
var obj = {
  number: 3,
  fn: (function () {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function () {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    }
  })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

//输出结果为:
//10
//9
//3
//27
//20

节流防抖

防抖

单位时间内,频繁触发事件,只执行最后一次
防抖的目的是确保函数在指定的延迟时间内没有被再次调用,才会执行。在搜索框搜索输入、按钮点击等需要短暂等待用户操作完成的场景中非常适用

js
function debounce (func, delay) {
  let timeId
  return function () {
    clearTimeout(timeId)
    timeId = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

节流

单位时间内,频繁触发事件,只执行一次。这在处理滚动、调整大小、按键等连续触发的事件时非常有用。通过节流,我们可以避免函数被过度调用

js
function throttle(func, delay) {
  let timerId
  return function () {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, arguments)
        timerId = null
      }, delay)
    }
  }
}

function throttle(func, delay) {
  let lastExec = 0
  return function () {
    const now = Date.now()
    if (now - lastExec >= delay) {
      lastExec = now
      func.apply(this, arguments)
    }
  }
}
最近更新