this绑定(call,apply,bind)

963 阅读9分钟

this绑定

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

this的四种绑定规则

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

注意:箭头函数不会创建自己的this,它只会从他的上一层作用域里继承this。

默认绑定

如果函数直接使用不带任何修饰的函数引用进行调用的,只能使用默认绑定,指向全局对象window,如果是在严格模式下this会绑定到undefined(因为此时不能把全局对象用于默认绑定)

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

隐式绑定

调用位置有上下文对象

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

注意

对象属性引用链只有上一层或者说最后一层在调用位置中起作用

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
}
var obj1 = {
    a: 2,
    obj2: obj2
}
obj1.obj2.foo(); //42

常见隐式绑定的函数丢失绑定对象,即运用了默认绑定

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

显式绑定

通过调用call,apply,bind方法

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
}
var bar = function() {
    foo.call(obj);
}
bar();   //2
setTimeout(bar, 100); //2

new绑定

javascript的new操作符和面向类的语言完全不同

在js中,构造函数只是一些使用new操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。

内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用

使用new调用函数会进行以下操作

  1. 创建一个全新的对象
  2. 这个对象会被执行函数的原型链所连接,即为新创建的对象添加__proto__属性
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有其他返回对象,那么new表达式中的函数调用会自动返回这个新对象。

new实现

语法:

new constructor[([arguments])]

实现

function mynew(fn) {
    //1.创建一个新对象
    let res = {};
    //2.连接原型
    if(fn.prototype !== null) {
        res.__proto__ = fn.prototype;
    }
    //3.绑定this后看是否返回其他对象
    let mayberes = fn.apply(res, [...arguments].slice(1));
    //4.进行判断
    if((typeof mayberes == "object" || typeof mayberes == "function") && mayberes !== null) {
        //4.1如果有返回其他对象,即返回它
        return mayberes;
    }
    //4.2没有就返回new创建的新对象
    return res;
}  
//---------------------------测试mynew
function Foo() {
    this.a = 2;
}
function Boo() {
    return {
        b: 2
    };
}
let obj = mynew(Foo); 
console.log(obj);   //Foo{a:2}
let obj2 = mynew(Boo);
console.log(obj2); //{b:2}
//---------------------------对比new
let obj1 = new Foo(); 
console.log(obj1);  //Foo{a:2}
let obj3 = new Boo();
console.log(obj3);  //{b:2}

优先级

结论:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

毫无疑问,默认绑定的优先级最低

隐式绑定与显示绑定的比较:

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

结果:显式绑定优先级大于隐式绑定

隐式绑定与new绑定的比较:

function foo(val) {
    this.a = val;
}
var obj = {
    foo,
};
obj.foo(2);
console.log(obj.a);   //2
obj1 = new obj.foo(4); 
console.log(obj1.a);   //4

结果:new绑定优先级大于隐式绑定

显式绑定与new绑定的比较

function foo(val) {
    this.a = val;
}
var obj = {};
var boo = foo.bind(obj);
boo(2);
console.log(obj.a);  //2
var obj1 = new boo(3);
console.log(obj1.a);  //3

new绑定优先级大于显式绑定

call,apply,bind

三者区别:

  1. call与apply的区别在于后面的参数,call接受的是一个参数列表,而apply接受的是一个包含多个参数的数组
  2. bind则是返回改变了上下文后的函数

call

语法:

function.call(thisArg, arg1, arg2, ...)

注意:

  1. thisArg:当thisArg为null或undefined时会自动替换成全局对象window
  2. 使用调用者提供的 this值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

实现

Function.prototype.myCall = function(context) {
    //由注意0可知
    context = context || window;
    //获取参数列表
    var args = [...arguments].slice(1);
    //添加方法1
    //context.fn = this;  //此处有漏洞,假如context里面本身就有fn方法,那么此时方法就被重写
    //添加方法2,利用ES6中Symbol的唯一性
    var fn = Symbol("juejin");
    context[fn] = this;
    //隐式绑定,this指向context,由注意1知返回值为该函数的返回值
    var res = context[fn](...args);
    //执行完毕后,需要删除方法
    delet context[fn];
    return res;
}
//----------------------------------测试
var name = "yly";
function foo(val) {
    console.log(this.name, val);
    return val;
}
var obj = {
    name: "dzz",
}
foo("diy");  //"yly" "diy"
console.log(foo.myCall(obj , "diy")); //"dzz" "diy"
//diy

apply

语法:

function.apply(thisArg, [argsArray])

注意:

  1. thisArg:当thisArg为null或undefined时会自动替换成全局对象window
  2. argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给调用函数。

实现:跟call差不多,差别在于传参不一样

Function.prototype.myApply = function(context) {
    context = context || window;
    //传入的参数可能为数组或者类数组还有可能不存在,都用扩展运算符转换成数组
    var args = arguments[1] ? [...arguments[1]] : [];
    var fn = Symbol("juejin");
    context[fn] = this;
    //传入给函数的参数还是以单个值传递
    var res = context[fn](...args);
    delete context[fn];
    return res;
}
//-------------------测试
function foo(val, val1) {
    console.log(this.name, val, val1);
    return val;
}
var name = "yly";
var obj = {
    name: "dzz",
}
var arr = [0,1];
console.log(foo.myApply(obj, arr));
//dzz 0 1
//0
console.log(foo.apply(obj, arr));
//dzz 0 1
//0

bind

语法:

function.bind(thisArg[, arg1[, arg2[, ...]]])

注意:

  1. 返回一个原函数的拷贝,并拥有指定this的值和初始参数,即会创建一个新的绑定函数
  2. 柯里化:把接受多个参数的函数变换成接受一个单一参数的函数,bind里面分了两次;
  3. 绑定函数可以使用new运算符进行构造,它会表现为目标函数已经被构建完毕了似的。提供的 this值会被忽略,但前置参数仍会提供给模拟函数。
  4. bind会继承函数原型上的属性

正题

简单实现1:由1知,这里需要用到myApply或者apply

Function.prototype.myBind = function(context) {
    //1.绑定当前this指向,防止返回函数的this指向错误(比如指向window,实际需要指向调用函数)
    var self = this;
    //2.返回拥有指定this的值和初始参数的函数
    return function() {
        //3.返回调用函数的返回值
        return self.myApply(context);
    }
}
//---------------------------------测试
var a = 1;
var b = 2;
function foo() {
    return this.a + this.b;
}
console.log(foo.myBind({a: 3, b: 4})()); //7

紧接着,我们需要实现柯里化

简单实现12:由2知,这里就简单实现将参数分成两次传入

Function.prototype.myBind = function(context) {
    //1.绑定当前this指向,防止返回函数的this指向错误(比如指向window,实际需要指向调用函数)
    var self = this;
    //2.获取除了context的参数
    var preArgs = Array.prototype.slice.call(arguments, 1);
    //3.返回拥有指定this的值和初始参数的函数
    return function() {
        //4.获取所有的参数
        var curArgs = [...preArgs, ...arguments];
        //5.返回调用函数的返回值
        return self.myApply(context, curArgs);
    }
}
//--------------------------测试
var a = 1;
var b = 2;
function foo(c, d) {
    return this.a + this.b + c + d;
}
console.log(foo.myBind({a: 3, b: 4}, 5)(6)); //18

紧接着实现3,将当前this指向实例,并且能够访问到原型链上的东西

简单实现123:思考要想new之后改变this指向,那么需要在返回的函数里对this进行判断,判断返回函数是否在当前this的原型链上,(即需要给函数命名,才可以接下来步骤)。

Function.prototype.myBind = function(context) {
    //1.绑定当前this指向,防止返回函数的this指向错误(比如指向window,实际需要指向调用函数)
    var self = this;
    //2.获取除了context的参数
    var preArgs = Array.prototype.slice.call(arguments, 1);
    //3.返回拥有指定this的值和初始参数的函数
    return function fb() {
        //4.获取所有的参数
        var curArgs = [...preArgs, ...arguments];
        //5.进行this的判断,即被当成构造函数时
        context = this instanceof fb ? this : context;
        //6.返回调用函数的返回值
        return self.myApply(context, curArgs);
    }
}
//--------------------------测试
var val = "window";
var obj = {
    val: "obj",
};
function foo(name) {
    this.like = "apple";
    console.log(this.val);
    console.log(name);
}
var tempFoo = foo.myBind(obj);
var boo = new tempFoo("dzz"); 
//------------------------bind测试
var tempFoo1 = foo.bind(obj);
var coo = new tempFoo1("dzz");  
​
//--------------------------控制台
//undefined
//dzz
//undefined
//dzz

最后实现4:这里绑定函数的原型可以使用Object.create()

最终完善1234

Function.prototype.myBind = function(context) {
    //1.绑定当前this指向,防止返回函数的this指向错误(比如指向window,实际需要指向调用函数)
    var self = this;
    //2.获取除了context的参数
    var preArgs = Array.prototype.slice.call(arguments, 1);
    //3.返回拥有指定this的值和初始参数的函数
    function fb() {
        //4.获取所有的参数
        var curArgs = [...preArgs, ...arguments];
        //5.进行this的判断,即被当成构造函数时
        context = this instanceof fb ? this : context;
        //6.返回调用函数的返回值
        return self.myApply(context, curArgs);
    }
    //继承原型链
    fb.prototype = Object.create(this.prototype);
    return fb;
}
//--------------------------测试
var val = "window";
var obj = {
    val: "obj",
};
foo.prototype.card = "名片";
function foo(name) {
    this.like = "apple";
}
var tempFoo = foo.myBind(obj);
var boo = new tempFoo("dzz"); 
console.log(boo.card);
//------------------------bind测试
var tempFoo1 = foo.bind(obj);
var coo = new tempFoo1("dzz");  
coo.card = "名片2";
console.log(coo.card);
​
//--------------控制台输出
//名片
//名片2

调用者必须是函数,判断

最终版本

Function.prototype.myBind = function(context) {
    //添加判断是否为函数
    if(typeof this !== "function") {
        throw new Error("this must be a function");
    }
    //1.绑定当前this指向,防止返回函数的this指向错误(比如指向window,实际需要指向调用函数)
    var self = this;
    //2.获取除了context的参数
    var preArgs = Array.prototype.slice.call(arguments, 1);
    //3.返回拥有指定this的值和初始参数的函数
    function fb() {
        //4.获取所有的参数
        var curArgs = [...preArgs, ...arguments];
        //5.进行this的判断,即被当成构造函数时
        context = this instanceof fb ? this : context;
        //6.返回调用函数的返回值
        return self.myApply(context, curArgs);
    }
    //继承原型链
    fb.prototype = Object.create(this.prototype);
    return fb;
}

额外:柯里化

柯里化

概念:将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

作用:使用柯里化可以避免不停地传入相同的参数

简单的实现

//1.传入函数
function currying(fn) {
    //2.获取currying的参数列表,没有fn
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        //3.获取接下来传入的参数
        var nextArgs = Array.prototype.slice.call(arguments);
        //4.将两次获取的参数合并成一个数组
        var finalArgs = args.concat(nextArgs);
        //5.利用apply将数组参数传入进去,返回调用函数的返回值
        return fn.apply(null, finalArgs);
    }
}
//---------------------------测试
function sum(a, b) {
    return a + b;
}
console.log(sum(2, 3));   //5未进行柯里化,需要传入两参数
var currySum = currying(sum);
console.log(currySum(2, 3));  //5
var currySum = currying(sum, 2);
console.log(currySum(3));    //5进行柯里化,可以断断续续传入参数
var currySum = currying(sum);
console.log(currySum(2));   //NaN

这只能分俩次进去传递进去,如果传参多次,就需要接着进行思考,添加判断进行递归看是否达到函数的传参值

具体的实现

//1.传入函数
function currying(fn) {
    //2.获取currying的参数列表,没有fn
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        //3.获取接下来传入的参数
        var nextArgs = Array.prototype.slice.call(arguments);
        //4.将两次获取的参数合并成一个数组
        var finalArgs = args.concat(nextArgs);
        //5.判断传入的参数个数是否到达要求函数需要传入个数
        if(finalArgs.length < fn.length) {
            //6.没有达到要求继续返回柯里化函数
            return currying(fn, ...finalArgs);
        }
        //7.利用apply将数组参数传入进去,返回调用函数的返回值
        return fn.apply(null, finalArgs);
    }
}
//---------------------------测试
function sum(a, b, c, d) {
    return a + b + c + d;
}
var currySum = currying(sum, 2);
console.log(currySum(2)(3)(4));  //11

结语

在面试的时候,面试官有时会问到this的绑定,以及让你自己手写一个call绑定出来等等。笔者就自己尝试着总结一些,提升下自己js基础能力。这篇文章相当于一篇总结文,希望看的小伙伴们收获满满。