JavaScript 深入理解this
this指向
this 不是静态的,this 在运行时才绑定,它的绑定和函数声明的位置没有关系,只取决于函数调用的方式
- 非严格模式下:this 不能是 undefined 或 null。当 this 指向为 undefined 或 null 时,this 会指向全局对象
- 严格模式下:this 会区分指向,正常指向 undefined 和 null
this 指向有一个优先级链,后者不能更改前者的 this 指向
箭头函数
>new关键字
>bind、apply、call
>obj.
>直接调用
改变this指向
call( )
call()
- 立即调用一个函数,并指定函数内部的
this
值 - 如果
call()
传入的this
上下文是undefined
或null
,那么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)
}
}
}