likes
comments
collection
share

this + call + apply + bind【JS深入知识汇点5】

作者站长头像
站长
· 阅读数 57

误解

this 机制在 javascript 中是动态绑定,或称为运行期绑定的。这就导致 JS 中的 this 关键字会有多重含义,所以会给我们造成一误解。学习 this 的第一步是明白this既不指向函数自身也不指向函数的词法作用域。

指向自身

人们容易把 this 理解成指向函数自身,看下面的代码

function foo(num) {
    console.log("foo: " + num
    this.count++;
}
// 这里为 foo 添加了一个属性 count,初始化为 0
foo.count = 0;
for(var i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log(foo.count); // 0
// foo 确实被调用了4次,但是 foo.count 还是 0,
// 是因为函数内部的 this 并不是指向那个函数对象,
// 所以虽然属性名相同,但是根对象却不相同

指向它的作用域

this指向函数的作用域,这个问题比较复杂,因为有时它是正确的,有时它的错误的。但可以明确的是:this在任何情况下,都不指向函数的词法作用域

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo();

结果是 undefined ,因为这里通过 this.bar() 来引用 bar 函数,这里能调用成功是因为:

  1. foo()被调用时,this === window,所以可以调用 bar 函数。
  2. bar执行时,this === window,使用对象属性访问规则,没有找到 a,所以返回undefined。

this 解析

javascript中,this 是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。

绑定规则

先根据把绑定的优先级抛出结论,按照以下顺序进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。

    var bar = new Foo()

  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话, this 绑定的是指定的对象。

    var bar = foo.call(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。

    var bar = obj1.foo()

  4. 如果都不是,使用默认绑定,严格模式下,绑定到 undefined,否则绑定到全局对象。

    var bar = foo()

new 绑定

在传统的面向类的语言中,构造函数是类的一些特殊方法,使用 new 初始化类时,会调用类中的构造函数。

首先,我们需要澄清的一个误解是:在JS中,是没有构造函数的,在普通的函数调用前面加 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造函数的形式来调用它

那么在JS中,使用 new 来调用函数,var obj = new Foo(),会执行以下操作:

  1. 首先创建一个全新的空对象。

    var o = new Object()

  2. 将空对象的原型 [[prototype]] 赋值为构造函数的原型.

    o.__proto__ = Foo.prototype

  3. 更改 this 的指向。

    Foo.call(o)

  4. 若 return 有值,并且值是对象,则直接返回此对象,否则,返回新创建的对象 o。

现在看一段简单的代码检测一下学习成果

function foo1(a) {
    this.a = a;
}
function foo2(name) {
    this.name = name;
    return {
        w:1
    };
}
foo2.prototype.getName = function() {
    return this.name;
};
var bar = new foo1(2);
console.log(bar.a) // 2
var a = new foo2('hh');
a.getName();
// Uncaught TypeError: a.getName is not a function
// 这是因为,foo2 有返回值,并且是一个对象,所以a值是 {w: 1}

显式绑定和硬绑定(call + apply + bind)

如果想调用函数时,强制把函数的 this 绑定到 obj 上,可以使用 call(...) 和 apply(...) 方法,它们的第一个参数一个对象,是给 this 准备的,这称之为显式绑定

apply 和 call 的作用一模一样,只是传参的形式有区别而已。

apply + call

apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组

call 方法传入两个参数:一个是作为函数上下文的对象,另一个是参数列表

两者的区别是:传入的第二个参数不同,apply 是传入带下表的集合,数组或类数组。call 从第二个开始传入的参数是不固定的,都会传给函数做参数。

call 的性能比 apply 好一些

apply 和 call 的用法
  • 改变 this 指向
function foo() {
    console.log(this.a)
}
var obj = {
    a: 2
};
foo.call(obj) //2

⚠️注意:如果传入了一个原始值(布尔、数字、字符串等类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new Boolean(...)、new Number(...)、new String(...))。

  • 借用别的对象的方法
var Person1  = function () {
    this.name = 'linxin';
}
var Person2 = function () {
    this.getname = function () {
        console.log(this.name);
    }
    // Person1的 this 变成了 Person 2 的this,并执行了一遍 Person1
    Person1.call(this);
}
var person = new Person2();
person.getname(); 
  • 调用函数 apply、call 方法都会使函数立即执行,所以也可以用来调用函数。call和apply没有参数,指向的是window。bibi
bind

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

call/apply 与 bind 的区别是:call、apply将立即执行该函数,bind 不执行函数,只返回一个可供执行的函数。

硬绑定就是显式绑定的一个变种,用于我们将 this 被永久绑定到 obj 的 foo 函数,这样我们就不必每次调用 foo 都在尾巴上加上 call 那么麻烦。

function foo() {
    console.log(this.a)
}
var obj = {
    a: 2
};
var bindFunc = foo.bind(obj) 
bindFunc(); //2
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
bind的模拟实现
console.log({}) // 在 prototype.constructor.prototype 上有 bind 函数
console.log(typeof Function.prototype.bind)   // 'function'
console.log(typeof Function.prototype.bind()) // 'function'
console.log(Function.prototype.bind.name)     // 'bind'
console.log(Function.prototype.bind().name)   // 'bound'

所以可得出结论:

  • bind 是 Function.prototype 的一个属性,每个函数都可以调用它
  • bind 是一个函数名为 bind 的函数,返回值也是一个函数,函数名是 bound。
var obj = {
    name: '程序媛兔纸'
}
function original(a, b) {
    console.log(this.name)
    console.log([a, b]);
    return false;
}
var bound = original.bind(obj, 1)
var boundResult = bound(2) // '程序媛兔纸' [1, 2]
console.log(boundResult)  // false
console.log(original.bind.name) // 'bind'
console.log(original.bind.length) // 1
console.log(original.bind().length) // 2,返回 original 函数的形参个数
console.log(bound.name) // 'bound original'
console.log((function(){}).bind().name) // 'bound '
console.log((function(){}).bind().length) // 0

可以得出结论:

  • 调用 bind 函数的 this 指向 bind() 函数的第一个参数
  • 传给 bind() 的其他参数接收处理了,bind() 返回的函数的参数也接收了,也就是合并处理了。
  • bind() 后的 name 为 bound + 空格 + 调用 bind 的函数名,如果是匿名函数,name 是 bound + 空格
  • bind() 的返回值,执行结果是原函数的返回值
  • bind 函数的形参是1,bind()的形参根据原函数的形参个数确定

所以我们先试着实现一个 bindFn

Function.prototype.bindTn = function(thisArg) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1)
    return function bound() {
        var params = Array.prototype.slice.call(arguments)
        self.apply(thisArg, args.concat(params))
    };
}

后续的new那部分的...过于复杂,我就不继续下去了,已经写完的够我现阶段使用

隐式绑定

当一个函数被一个对象调用时,会把函数调用中的 this 绑定到这个上下文对象中。

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

⚠️注意:当我们使用隐式绑定规则时,要注意下面两点:

  • 在参数传递、赋值时,隐式绑定的函数可能会丢失绑定对象,也就是会应用默认绑定规则。
    function foo() {
       console.log(this.a);
    }
    function doFoo(fn) {
       fn();
    }
    var obj = {
       a: 2,
       foo: foo
    };
    var a = 'oops, global';
    // obj.foo 引用 foo 函数本身,应用默认绑定
    doFoo(obj.foo); // oops, global
    
    var bar = obj.foo;
     // bar 引用 foo 函数本身,应用默认绑定
    bar(); // oops, global
    
  • 对象属性引用链中只有最后一层在调用位置中起作用。

默认绑定

当直接使用不带任何修饰的函数引用进行函数调用时,只能使用默认绑定,无法应用其他规则。在严格模式下,this 会绑定到 undefined,在非严格模式下, this会绑定到全局对象。

如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply或者 bind,会应用默认绑定规则。

箭头函数

箭头函数根据外层(函数或全局)作用域来决定 this。

var obj = {
    age : 20,
    say : () => {
        alert( this.age )
    }
}
obj.say();  // undefined  因为obj对象不能产生作用域,所以箭头函数相当于定义在全局作用域,this指向全局
function foo() {
    return (a) => {
        //  继承自 foo()
        console.log(this.a)
    };
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
// 这是因为箭头函数根据外层的作用域来决定this,此处的语句只能让函数执行
bar.call(obj2) // 2

好题练习

练习题1:

function Foo() {
    getName = function() {
        console.log(1);
    };
    return this;
}
// 静态方法赋值
Foo.getName = function() {
    console.log(2);
};
Foo.prototype.getName = function() {
    console.log(3);
};
var getName = function() {
    console.log(4);
};
function getName() {
    console.log(5);
}
Foo.getName(); //2

// 变量声明提升,赋值语句留在原地,所以结果是4
getName(); //4

// 在Foo函数中,重写了全局作用域下的 getName 函数
Foo().getName(); //1 
getName(); //1

// 等价于new (Foo.getName)(), 运算符优先级:成员访问 > new(不带括号) > 函数调用
new Foo.getName(); //2 

// 等价于 (new Foo()).getName(),运算符优先级:成员访问、new(带括号)的优先级一样,所以从左到右执行。
// 使用 new 实例 Foo 后生成的实例上,只有原型方法,没有静态方法
new Foo().getName();

练习题2:

var title = 'world';
var a = {
    title: 'hello',
    alias: this.title,
    show() {
        console.log(this.title, this.alias)
    },
    ashow: () => {
        console.log(this.title, this.alias)
    }
}

a.show(); // hello world
var b = a.show; 
b(); // world undefined
a.ashow(); // world undefined

练习题3:

var b = 20;
var a = {
  b: 15,
  fn: function() {
    var b = 30;
    return function() {
      return this.b;
    };
  }
};
// 未指定函数的调用者的话,this就绑定到全局对象上。
console.log(a.fn()()); // 20

练习题4:

var b = 20;
var a = {
  b: 15,
  fn: function() {
    var b = 30;
    return function() {
      return this.b;
    };
  }
};
// 未指定函数的调用者的话,this就绑定到全局对象上。
console.log(a.fn()()); // 20

练习题5:

var a = 'javascript';
var obj = {
    a : 'php',
    prop : {
        getA : function() {
            return this.a;
        }
    }
}

console.log(obj.prop.getA());
var text = obj.prop.getA;
console.log(text());
// undefined  javascript

练习题6:

var x = [typeof x , typeof y][1]; // undefined
console.log(typeof typeof x); // typeof undefined 是‘undefiend’,typeof ‘undefiend’ 是 string

练习题7:

var obj1 = {
  name: "blue",
  fn: function(){
      console.log(this.name);
      }
};
var newFunction = obj1.fn;
newFunction();
// 结果是空,因为window有name属性,是空,如果是别的变量,就可能是 undefined

练习题8:

var age = 38;
var obj = {
  age:18,
  getAge: function () {
    console.log(this.age);

    function foo(){
      console.log(this.age);
    }
    foo();
  }
}
obj.getAge();  // 18  38

练习题9:

var length = 10;
function fn() {
    console.log(this.length); 
}
var arr = [fn, "222"];
fn(); // 10
// arr[0]方式的调用,使用隐式绑定的方式,this就是arr
arr[0](); // 2

练习题 10:

var length = 10
function fn() {
    console.log(this.length)
}
var obj = {
    length: 5,
    method: function (fn) {
        fn();    
        arguments[0]();  
    }
}
obj.method(fn, 10, 5, 2, 6); 
// 10
// 5 是因为 arguments[0]() 时,使用隐式绑定的方式,此时 arguments 是[fn, 10, 5, 2, 6] ,arguments.length 是 5
转载自:https://juejin.cn/post/6844904164129013774
评论
请登录