面试题之:JavaScript中this以及apply/call/bind的用法

2022-10-28 18:00:28 浏览数 (2)

什么是函数的调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)

为什么要了解调用位置:只有了解函数的调用位置才能进一步的确定 this 的绑定对象

代码语言:javascript复制
function baz () {
  // 当前调用栈是:baz, 因此,当前调用位置是全局作用域 
  console.log("baz");
  bar(); // bar 的调用位置
}

function bar () {
  // 当前调用栈是 baz -> bar,因此,当前调用位置在 baz 中 
  console.log("bar");
  fnn(); // fnn 的调用位置
}

function fnn () {
  // 当前调用栈是 baz -> bar -> fnn, 因此,当前调用位置在 bar 中 
  console.log("fnn");
}
baz(); // baz 的调用位置

this 是什么

this 是包含它的函数作为方法被调用时所属的对象。

  • 包含它的函数。
  • 作为方法被调用时。
  • 所属的对象。

随着函数使用场合的不同,this 的值会发生变化。this 指向什么,完全取决于什么地方以什么方式调用,而不是创建时。

this 的四种绑定规则

this4 种绑定规则分别是:默认绑定、隐式绑定、显式绑定、new 绑定。优先级从低到高。

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

默认绑定

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

如代码(非严格模式下)和图片所示

  • this.a 被解析成了 window.afnn 中的 this 是等于 window 的。
  • 由于 fnn() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则,因此 this 指向全局对象。。
代码语言:javascript复制
function fnn() { 
  console.log(this);
  console.log(this === window);
  console.log(this.a); 
}
var a = 2; 
fnn(); // 2

隐式绑定

调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含

如代码(非严格模式下)和图片所示

  • this.a 被解析成了 obj.afnn 中的 this 是等于 obj 的。
  • 由于调用 fnn() 函数时,有引用上下文对象 obj,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象 obj,因此 this 指向 obj 对象。。
代码语言:javascript复制
function fnn() { 
  console.log(this);
  console.log(this === obj);
  console.log(this.a); 
}
var obj = { 
  a: 2, 
  fnn: fnn 
};
  
obj.fnn(); // 2

对象属性引用链中只有最顶层或者说最后一层会影响调用位置

如代码所示,最后输出的 this.a 等于 888,因为最后调用 fnn 的上下文对象是 obj1,所以 this 绑定在 obj1

代码语言:javascript复制
function fnn() { 
  console.log(this);
  console.log(this === obj1); // true
  console.log(this.a); // 888
}

var obj1 = { 
  a: 888, 
  fnn: fnn 
};

var obj = { 
  a: 2, 
  obj1: obj1 
};

obj.obj1.fnn(); // this.a 输出 888
隐式丢失

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

如代码所示,最后输出的 this.a 等于 888,虽然 barobj.fnn 的一个引用,但是实际上,它引用的是 fnn 函数本身, 因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

代码语言:javascript复制
function fnn() { 
  console.log(this); // window 对象
  console.log(this == window); // true
  console.log(this.a); // 888
}
var obj = { 
  a: 2, 
  fnn: fnn 
};
  
var bar = obj.fnn;

var a = "888"; // a 是全局对象的属性 
bar(); // 888

回调函数中的例子

代码语言:javascript复制
function fnn() { 
  console.log(this); // window 对象
  console.log(this == window); // true
  console.log(this.a); // 888
}

function doFnn(fn) { 
  // fn 其实引用的是 fnn 
  fn(); // <-- 调用位置!
}

var obj = { 
  a: 2, 
  fnn: fnn 
};
  
var a = "888"; // a 是全局对象的属性 
doFnn(obj.fnn); // 888

显式绑定

使用 callapply 或者 bind 方法绑定

call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

代码语言:javascript复制
function fnn(arg1, arg2) { 
  console.log(this);
  console.log(this === obj); // true
  console.log(this.a); // 888
  console.log(arg1, arg2); // 参数一 参数二
}
var obj = { 
  a: 888, 
  fnn: fnn 
};
var a = 2
fnn.call(obj, '参数一', '参数二'); // 888

apply

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

代码语言:javascript复制
function fnn(arg1, arg2) { 
  console.log(this);
  console.log(this === obj); // true
  console.log(this.a); // 888
  console.log(arg1, arg2); // 参数一 参数二
}
var obj = { 
  a: 888, 
  fnn: fnn 
};
var a = 2
fnn.apply(obj, ['参数一', '参数二']); // 888

bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

代码语言:javascript复制
function fnn(arg1, arg2) { 
  console.log(this);
  console.log(this === obj); // true
  console.log(this.a); // 888
  console.log(arg1, arg2); // 参数一 参数二
}
var obj = { 
  a: 888, 
  fnn: fnn 
};
var a = 2
const fnnBind = fnn.bind(obj, '参数一', '参数二'); 
fnnBind() // 888

::: tip bindapplycall 的区别是,bind 是创建一个函数,但不会直接调用 :::

new 绑定

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

代码示例

代码语言:javascript复制
// 声明一个构造函数
function People(name){
  console.log(fnn != this); // true
  this.name = name;
}
 
// 使用 new 创建实例对象 fnn
var fnn = new People("FX");
console.log(fnn);
console.log(fnn.name) // FX;

如代码和图片所示,我们 new 一个实例对象,代码执行过程如下

代码语言:javascript复制
var fnn = {} // 创建一个空对象; 或者 var fnn = new Object() 
fnn.__proto__ = People.prototype // 将该对象 fnn 的隐式原型指向构造函数显式原型
People.call(fnn, "FX") // 将构造函数中 this 指向创建的对象 fnn,并传入参数 "FX"
return fnn // 返回对象 fnn,person 指向创建的对象 fnn(对象类型赋值为按引用传递,fnn 与 person 指向同一个对象)

::: danger 为什么 console.log(fnn != this) 的值为 true,关于这一点我还不是特别理解。 按照我得想法,可能是在 new 对象实例的过程中,实例对象实际并没有创建完毕,导致的不相等,如果有更好的理解,欢迎大家留言。 :::

new 绑定遇到 retrun

代码语言:javascript复制
function fnn () {
    this.user = 'fx'
    return {}
}
var a = new fnn()
console.log(a.user) // undefined
代码语言:javascript复制
function fnn () {
    this.user = 'fx'
    return function () {
    }
}
var a = new fnn()
console.log(a.user) // undefined
代码语言:javascript复制
function fnn () {
    this.user = 'fx'
    return 1
}
var a = new fnn()
console.log(a.user) // fx
代码语言:javascript复制
function fnn () {
    this.user = 'fx'
    return undefined
}
var a = new fnn()
console.log(a.user) // fx
代码语言:javascript复制
function fn () {
    this.user = 'fx'
    return null
}
var a = new fn
console.log(a.user) // fx

如果返回值是一个对象,那么 this 指向的就是那个返回的对象,如果返回值不是一个对象那么 this 还是指向函数的实例。 还有一点就是虽然 null 也是对象,但是在这里 this 还是指向那个函数的实例。

优先级测试

显示绑定 与 隐式绑定

如下代码,可以看出 显示绑定 优先级大于 隐式绑定

代码语言:javascript复制
function fnn () {
  console.log(this.a)
}

var obj1 = {
  a: 2,
  fnn: fnn
}
var obj2 = {
  a: 3,
  fnn: fnn
}
obj1.fnn() // 2 
obj2.fnn() // 3 
obj1.fnn.call(obj2) // 3 
obj2.fnn.call(obj1) // 2

new 绑定 与 隐式绑定

如下代码,可以看出 new 绑定 优先级大于 隐式绑定

代码语言:javascript复制
function fnn (num) {
  this.a = num
}

var obj1 = {
  a: 2,
  fnn: fnn
}
var bar = new obj1.fnn(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4

new 绑定 与 显示绑定

如下代码,可以看出 new 绑定 优先级大于 显示绑定

代码语言:javascript复制
function fnn (num) {
  this.a = num
}

var obj1 = {
  a: 2,
}
var bar = fnn.bind(obj1);
bar(888)
console.log(obj1.a); // 888

var baz = new bar(666); 
console.log(obj1.a); // 888
console.log(baz.a); // 666

this 丢失的情况

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

代码语言:javascript复制
function fnn() { 
  console.log(this.a); 
}

var a = 2; 
fnn.call(null); // 2

间接引用的问题

如下代码,赋值表达式 p.fnn = o.fnn 的返回值是目标函数的引用,因此调用位置是 fnn() 而不是 p.fnn() 或者 o.fnn(),这里会应用默认绑定

代码语言:javascript复制
function fnn () {
  console.log(this.a)
}

var a = 2
var o = {
  a: 3,
  fnn: fnn
}
var p = {
  a: 4
}
o.fnn(); // 3 
(p.fnn = o.fnn)() // 2

箭头函数的 this

如下代码所示,fnn() 内部创建的箭头函数会捕获调用时 fnn()this。由于 fnn()this 绑定到 obj1bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

代码语言:javascript复制
function fnn () {
  // 返回一个箭头函数
  return () => {
    // this 继承自 fnn()
    console.log(this.a)
  }
}

var obj1 = {
  a: 2
}
var obj2 = {
  a: 3
}
var bar = fnn.call(obj1)
bar.call(obj2) // 2

非箭头函数,输出 3

代码语言:javascript复制
function fnn () {
  // 返回一个箭头函数
  return function () {
    // this 继承自 fnn()
    console.log(this.a)
  }
}

var obj1 = {
  a: 2
}
var obj2 = {
  a: 3
}
var bar = fnn.call(obj1)
bar.call(obj2) // 3

测试题

示例一

代码语言:javascript复制
var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); // 12
        }
    }
}
o.b.fn(); // 12
 
var o = {
    a:10,
    b:{
        // a:12,
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn(); // undefined

示例二

代码语言:javascript复制
var o = {
    a: 10,
    b: {
        a: 12,
        fn: function () {
            console.log(this.a) //undefined
            console.log(this) //window
        }
    }
}
var j = o.b.fn
j()

示例三

代码语言:javascript复制
var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x) // 20
        function fnn () {
            console.log(this.x)
        }
        fnn() // 10 默认绑定,这里 this 绑定的是 window
    }
}
obj.f()

示例四

  • fnn(1) 使用默认绑定,this.a = arg 相当于 window.a = argreturn this 相当于 return window
  • var a = fnn(1) 相当于 window.a = window,所以 a.a 等于 window,依次类推 a.a.a.a 仍旧是 window
  • fnn(10) 使用默认绑定,this.a = 10 相当于 window.a = 10return this 相当于 return window
  • console.log(b.a) 相当于 window.a 等于 10
代码语言:javascript复制
function fnn (arg) {
  this.a = arg
  return this
}
var a = fnn(1)
console.log(a.a) // window 

var b = fnn(10)

console.log(b.a) // 10

示例五

代码语言:javascript复制
var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x)
    }
}
var bar = obj.f
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f() // 20
bar() // 10
obj2.f() // 30

0 人点赞