JavaScripts高阶(10)call、apply、bind(都是用来改变某一个函数中this指向的)

1,108 阅读8分钟

必须是函数才能使用call、apply、bind,也就是.前边必须是函数,call、apply、bind中的this就是该函数

call、apply是把this(需要改变this指向的函数、call、apply前边的函数)绑定给context(要指向的this)的一个fn属性然后 context.fn()

bind利用闭包把this(需要改变this指向的函数、bind前边的函数)传递到返回的函数,在执行时

  1. apply实现: 直接this.appl改变this的指向到context(要指向的this)
  2. es手动实现:也是和call、apply一样绑定给context(要指向的this)的一个fn属性然后 context.fn()

call:fn.call(arg1, arg2, ...);

第一个参数必须是对象数据类型

fn.call :(找到call方法)当前实例(函数FN)通过原型链的查找机制找到Function.prototype上的call方法 => function call(){[native code]}

fn.call():把通过原型链找到的call方法执行,当call方法执行的时候,内部处理了一些事情

  1. 首先把要操作函数中的this变为call方法第一个传递的实参值
  2. 把call方法第二个以后的实参(包括第二个)获取到
  3. 把要操作的函数执行并且把第二个以后传递进来的实参(包括第二个)传给函数

非严格模式下,如果参数不传,或者第一个传递的是null/undefined,this都指向window

严格模式下,传递的第一个参数是谁this就指向谁,包括null和undefined;不传this就是undefined

返回值:使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined。

//call原理
fn.call(arg1, arg2, ...);
Function.prototype.call=function(){
	let param1=arguments[0];
	let paramOther=[] ;  //把arguments中除了第一个以外的实参获取到

	//=> call中的this:fn  当前要操作的函数(函数类的一个实例)
	//1、把fn(也就是call中的this)中的this关键字修改为param1 => 把this(call中的this)中的this关键字修改为param1
	//把fn(this)执行,paramOther(第二个及以后的参数)传给fn
	//this(paramOther)
}

window.name='aaa';
let fn=function (){
	console.log(this.name)
}
let obj={
	name:'OBJ',
	fn:fn
}

let oo={
	name:'oo'
}

fn();  //this:window   'aaa'
obj.fn();  // this:obj    'OBJ'

oo.fn()    //报错

fn.call(oo)   //this:oo   'oo'
fn.call(obj)   //this:obj  'OBJ'

手动实现call了解其原理

  • 第一步:将函数设为传入对象的属性
  • 第二步:执行该函数
  • 第三部:删除该函数
Function.prototype.myCall = function(context) {
    // 首先要获取调用call的函数,用this可以获取(原型中的this是.前面的函数)
    context.fn = this;
    context.fn();      //在context上调用函数,那函数的this值就是context.
    delete context.fn;
}

接受任意参数有两种思路:

  1. 处理arguments (原生js)拼出一个字符串,像这样 eval("fn(arguments[1], ...,arguments[n])") 然后用eval执行

  2. 解构运算符 (ES6)

call(target, ...args)
Function.prototype.myCall = function (context, ...args) {
  context = context ? Object(context) : window;
  
  // 用this获取调用当前myCall的方法  再绑定到 context 上
  context.fn = this
  // 获取 传入的参数 (从arguments对象里取)
  const val = context.fn(...args);
  
  // 删除 context 上添加的方法
  delete context.fn

  return val
}

最终代码

Function.prototype.myCall = function(context){
    if(context === null || context === undefined){
        context = window;
    } else {
        context = Object(context);
    }
    let arg = [];
    let val ;
    // i 从1开始只取第二个和以后的作为参数
    for(let i = 1 ; i<arguments.length ; i++){
        arg.push( 'arguments[' + i + ']' ) ;
    }
    // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]", ...]
    context._fn_ = this;
    val = eval( 'context._fn_(' + arg + ')' ) 
    delete context._fn_;
    return val
}

call.call 原理

一个call是让call前边的函数执行;两个及以上 的call是让call后面括号里的函数执行,如果call后面括号里的不是函数报错

Function.prototype上的call方法(也是一个函数,也是函数类的实例,也可以继续调用call、apply、bind等方法)所以:无论调用几次call实际只最后的执行call.call()

let sum=function(a,b){
    console.log(this)
}
let sum2=function(a,b){
    console.log(this)
    console.log(555)
}
let opt={n:20}
sum.call(opt,20,30); 
//浅显 //sum中的this:opt  a=20 b=30
//深入 //call执行 call中的this是sum,把this(call中的this也就是sum)中的“this关键字”改为opt
sum.call.call(opt)
//1、sum.call找到Function.prototype上的call方法(也是一个函数,也是函数类的实例,也可以继续调用call、apply、bind等方法)    =》A:sum.call
//2、A.call(opt)    继续找到原型上的call方法,把call方法执行:call中的this变为A,A中的this修改为opt然后把A执行(执行的是原型上的call)  报错sum.call.call is not  a  function

sum.call.call(sum2)  //555 this:window
//sum.call => 先找到Function.prototype.call => call.call(sum2)
//在第二个call中 context为sum2、context._fn_ 为call  最终是  sum2.call()
function fn1(){console.log(1)}
function fn2(){console.log(2)}

fn1.call(fn2);    					//1
	//找到callAA并执行,callAA中的this是fn1,fn1中的this为fn2,callAA中执行的是fn1
fn1.call.call(fn2);   				//2
	//fn1.call => 先找到Function.prototype.call => call.call(fn2)
        //在第二个call中 context为fn2、context._fn_ 为call  最终是  fn2.call()
	//=>让fn2中的this变为undefined,因为fn1.call的时候没有传递参数值,
	//然后让fn2执行
Function.prototype.call(fn1)		//Function.prototype()   没有输出
Function.prototype.call.call(fn1)	//fn1()   1

call继承父级属性用法:父类.call(this,arg1,arg2...)

在一个子构造函数中,你可以通过调用父构造函数的call方法来实现继承

下例中,使用Food和Toy构造函数创建的对象实例都会拥有在Product构造函数中添加的name属性和price属性,但category属性是在各自的构造函数中定义的

function Product(name,price) {
    this.name=name;
    this.price=price;
}
function Food(name,price) {
    Product.call(this,name,price);
    this.category='food';
}
function Toy(name,price) {
    Product.call(this,name,price);
    this.category='toy';
}
var cheese = new Food('aaa',5)
var fun = new Toy('bbb',20)
console.log(cheese.name);   //aaa
console.log(fun.price);     //20
console.log(cheese.name == fun.name);  //false

console.log(cheese.category);   //food
console.log(fun.category);   //toy

console.log(cheese.category == fun.category); //false

apply:和call基本一样,只有一个区别:传参方式,传递给fn的参数(第一个以后的所有参数)必须放在一个数组中

apply把需要传递给fn的参数(第一个以后的所有参数)放在一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递

fn.call(obj,10,20)

fn.apply(obj,[10,20])

手动实现apply

Function.prototype.myApply = function(context,arr){
    if(context === null || context === undefined){
        context = window;
    } else {
        context = Object(obj);
    }
    let args = [];
    let val ;
    // i 从0 开始 因为 arr就是参数集合
    for(let i = 0 ; i<arr.length ; i++){
        args.push( 'arr[' + i + ']' ) ;
    }
    context._fn_ = this;
    val = eval( 'context._fn_(' + args + ')' ) 
    delete context._fn_;
    return val
}

bind:语法和call一样,唯一的区别:立即执行还是等待执行调用方法的函数

fn.call(obj,10,20) 改变fn中的this,并让fn立即执行

fn.bind(obj,10,20) 改变fn中的this,fn不执行,必须手动调用fn,fn才会执行(不兼容IE6-8)

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数

bind 第一个参数是一个函数, 在执行的时候也是执行的也是bind的第一个参数

  1. bind返回一个函数,该函数执行时使用给定的this执行
  2. bind可以接受参数,在bind缓存arguments,用闭包绑到boundF中,和boundF接受的参数合并(boundF是返回的函数)

注意:返回一个函数,那就是说可以被new,如果被new的话,this不应该指向给定的this,因为如果new的this指向会被改变的话,实例会有问题。

var module = {
  x: 42
}

var unboundGetX =  function() {
    return this.x;
}

var boundGetX = unboundGetX.bind(module);
console.log(boundGetX());   // 42
function fn(a,b){
	console.log(this,a,b)
}

var obj={name:'aaa'}


document.onclick=fn;    //把fn绑定给点击事件,点击执行

document.onclick=fn();  //在绑定的时候,先把fn执行,把执行的返回值绑定给事件,当点击的时候执行的是undefined

手动实现bind

调用时使用指定this,返回的函数中用call/apply就好了

bind 可以接收两次参数:1、bind时的传参;2、执行时的传参

需要将两次调用的传参一起传进去

// 第一版
Function.prototype.myBind = function (context) {
    var self = this;
    // 获取myBind函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数 (就是 函数执行时的传参)
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }

}

处理new的情况

示例
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
new的表现
  1. this指向了boundF正在构造的新对象
  2. boundF的prototype在新对象的原型链上
解决思路
  1. 判断是不是被new了,是的话把this还给新对象,即判断fBound是不是在新对象(this)的原型链上
  2. 保证new出来的对象原型链和原函数一致而不是和fBound一致,把原函数的原型链搞到fBound的prototype上即可
// 第二版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

使用apply最终实现

Function.prototype.myBind = function (context) {
    // this指向原函数
    var that = this
    // 取出bind的参数,构造闭包,因为arguments在boundF中会被覆盖访问不到了
    var args = Array.prototype.slice.apply(arguments, 1) 
    // 上边的操作相当于 arguments.slice(1) 区第二个及以后的参数
    
    var boundF =  function () {
        // 这里的arguments是boundF的arguments, 也就是实际调用时的参数
        var bindArgs = Array.prototype.slice.call(arguments)
        // 这里的this有两种:
        //      boundF (普通调用)
        //      newObj (new)
        // 注意boundF instanceof boundF是false的
        return that.apply(this instanceof boundF ? this : context, args.concat(bindArgs))
    }
    // 切换原型链
    boundF.prototype = Object.create(this)
    return boundF
}

ES6

Function.prototype.myBind = function(context,...arg1){
    return (...arg2) => { 
        let args = arg1.concat(arg2);
        let val ;
        context._fn_ = this;
        val = context._fn_( ...args ); 
        delete context._fn_;
        return val
    }
}

ES5

Function.prototype.myBind = function(context){
    if(context === null || context === undefined){
        context = window;
    } else {
        context = Object(context);
    }
    let _this = this;
    let argArr = [];
    for(let i = 1 ; i<arguments.length ; i++){
        argArr.push( 'arg1[' + (i - 1)  + ']' ) ;
    }
    return function(){
        let val ;
        for(let i = 0 ; i<arguments.length ; i++){
            argArr.push( 'arguments[' + i + ']' ) ;
        }
        context._fn_ = _this;
        console.log(argArr);
        val = eval( 'context._fn_(' + argArr + ')' ) ;
        delete context._fn_;
        return val
    };
}

获取数组中的最大值

let arr=[12,13,14,15,16,23,24,13,12,15]

//=>方法一:从大到小排序,取第一个
let max=arr.sort(function(a,b){
	return b-a;
})[0];

//=>方法二:遍历比大小
let max=arr[0]
for(var i=1;i<arr.length;i++){
	if(arr[i]>max){
		max=arr[i];
	}
}

//方法三:基于eval、Math.max;Math.max不能直接把数组传进去;拼成"Math.max(12,13,14...)"
let str="Math.max("+arr.toString()+")";
eval(str);


//方法四:apply的语法:apply把需要传递给fn的参数放在一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递

Math.max.apply(null,arr)

//方法5:利用ES6的展开运算符和Math.max

Math.max(...arr)



参考: github.com/mqyqingfeng…