A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 专注的一批 中级黑马   /  2019-10-17 14:15  /  1096 人查看  /  0 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

不知其理,何以谈用,在这里简单记录一下个人对call、apply、bind的理解,并根据理解思路实现一下。
众所周知 call、apply、bind 的作用都是‘改变’作用域,但是网上对这这‘改变’说得含糊其辞,并未做详细说明,‘改变’是直接替换作用域?谁替换谁?怎么产生效果?这些问题如果不理解清楚,就算看过手写实现,估计也记不长久,基于此,这里做简单记录,以免时间过长遗忘,方便回顾。
call,apply,bind是Function.prototype原型上的方法
任何函数都可以访问call,apply,bind方法(原因:任何函数的原型链上都有Function.prototype原型)

call,apply的作用
调用函数
//除了使用函数名() 来调用函数外,还可以使用call,apply来调用函数
function fn(){
    console.log(123);
}
fn.call();//123
fn.apply();//123
传参数
function sum(x, y) {
    console.log(x + y);//5
}
var arr = [2, 3]
//传入null/undefined的时候将执行js全局对象,浏览器中是window
sum.apply(null, arr)
var arr = [18, 29, 89, 73, 5, 10, 8, 99, 105, 52];
Math.max.apply(null,arr)//105
修改this指向
var dog = {
    name: '小黄',
    age: 3
}
function animal () {
    console.log(this);//window
    console.log(this.name);//'' 全局变量有name属性,所以不为undefined
    console.log(this.age);//undefined
}
animal()//函数调用模式,this指向window
var dog = {
    name: '小黄',
    age: 3
}
function animal () {
    console.log(this);//对象dog
    console.log(this.name);//'小黄'
    console.log(this.age);//3
}
animal.call(dog)//修改this指向为对象dog
借用对象方法
伪数组与数组
伪数组也叫类数组伪数组其实就是一个对象,但是跟数组一样,伪数组也会有`length`属性,也有`0,1,2,3`等属性
伪数组并没有数组的方法,不能使用`push/pop`等方法
伪数组可以跟数组一样进行遍历,通过下标操作。
function(){   //外汇返佣:http://www.kaifx.cn
常见的伪数组:`arguments`、`document.querySelectorAll`的返回值、`jQuery对象`
//创建一个伪数组
var obj = {
    0: "卡卡西",
    1: "佐助",
    2: "鸣人",
    length: 3
};//分号
//借用数组的方法,往伪数组最后添加一项
//Array.prototype.push.call(obj, "小樱");
[].push.call(obj, "小樱");//注意这种写法必须在对象后面添加分号,不然会被浏览器当做点语法解析
var obj = {
    0: "卡卡西",
    1: "佐助",
    2: "鸣人",
    length: 3
};
//通过silice截取数组可以将伪数组转换成真数组
var arr = [].slice.call(obj);
console.log(arr);//["卡卡西", "佐助", "鸣人"]
//数组借用Math对象方法求最大值
var arr = [18, 29, 89, 73, 5, 10, 8, 99, 105, 52];
var ret = Math.max.apply( arr, arr);//借用Math对象中max求最大值的方法
console.log(ret);//105
call,apply使用场景:
如果参数比较少,使用call会更加简洁
如果参数存放在数组中,此时需要使用apply
bind的作用
简单介绍bind的作用,想具体了解bind如何使用,戳这里
var fn = function () {
     console.log(this);//window
}
fn(); //函数调用模式,this指向window
//fn和fn2长的一样,但是在内存中是两份函数,地址是不一样。
var fn = function () {
     console.log(this);//[10, 20, 30]
}
var fn2 = fn.bind( [10, 20, 30] ); // fn2 是创建的新函数,新函数和fn长的一样。
console.log(fn === fn2); // false
//fn和fn2长的一样,但是在内存中是两份函数,地址是不一样。
fn2();
//fn2函数是由bind创建出来的, fn2函数内的this指向被固定了,所以this指向了[10, 20, 30]
//固定的理解: 不论fn2 函数的调用模式是何种,fn2内的this指向被固定写死了。
既然 call/apply 和 bind 的功能如此相似,那什么时候该使用 call、apply,什么时候使用 bind 呢?其实这个也没有明确的规定,一通百通而已,只要知其理,相互转化何其简单,主要的区别无非就是 call/apply 绑定后是立即执行,而 bind 绑定后是返回引用待调用
就像这样
const people = {
    age: 18
};
const girl = {
    getAge: function() {
        return this.age;
    }
}
console.log('输出:' + girl.getAge.bind(people)());  // 输出:18
console.log('输出:' + girl.getAge.call(people));    // 输出:18
console.log('输出:' + girl.getAge.apply(people));   // 输出:18


一次看到个有趣的问题是如果多次 bind 呢,会有什么效果?
const people1 = {
    age: 18
}
const people2 = {
    age: 19
}
const people3 = {
    age: 20
}
const girl = {
    getAge: function() {
        return this.age
    }
}
const callFn = girl.getAge.bind(people1)
const callFn1 = girl.getAge.bind(people1).bind(people2)
const callFn2 = girl.getAge.bind(people1).bind(people2).bind(people3)
console.log(callFn(), callFn1(), callFn2())
// 18 18 18
这里都输出 18 ,而没有期待中的 19 和 20 ,原因是在 Javascript 中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次及以后的 bind 是无法生效的
再看一段示例
const tempFn = function () {
    console.log(this, [...arguments])
}
const cont1 = tempFn.bind({
        name: '渣渣逆天'
    }, 1)
cont1.call({
        age: 24
    }, 2)
// {name: "渣渣逆天"}  [1]
const cont2 = cont1.bind({
        apper: 'bueaty'
    }, 2)
cont2()
// {name: "渣渣逆天"}  [1, 2]
const cont3 = cont2.bind({
        fat: 'thin'
    }, 3)
cont3()
// {name: "渣渣逆天"}  [1, 2, 3]
从上面的代码执行结果中我们发现一点,第一次 bind 绑定的对象是固定的,也就是后面通过 bind 或者 call 再次绑定的时候,就无法修改这个 this 了,从 ES5 文档中我们能找到答案
When the [[Call]] internal method of a function object, F, which was created using the bind function is called with a this value and a list of arguments ExtraArgs, the following steps are taken:
Let boundArgs be the value of F’s [[BoundArgs]] internal property.
Let boundThis be the value of F’s [[BoundThis]] internal property.
Let target be the value of F’s [[TargetFunction]] internal property.
Let args be a new list containing the same values as the list boundArgs in the same order followed by the same values as the list ExtraArgs in the same order.
Return the result of calling the [[Call]] internal method of target providing boundThis as the this value and providing args as the arguments.
这段话中说到如果我们在一个由 bind 创建的函数中调用 call,假设是 x.call(obj, y, z, …) 并且传入 this,和参数列表的时候会执行下面的步骤:
首先用三个参数分别保存函数x函数的内部属性中存的this值、目标函数和参数 列表。
然后执行目标函数的内部 call 函数,也就是执行目标函数的代码,并且传入1中保存的 this 和实参(这里的实参是目标函数本来就有的也就是 bind 时传入的实参加上调用 call 时传的实参)
重点在1中,从 ES5 的 bind 函数说明中我们知道,当我们用一个函数调用 bind 的时候,返回的函数中会保存这三个参数。所以最后调用 call 的时候执行的函数是目标函数,也就是调用了 bind 的函数,传入的 this 也是 bind 调用时传入的,这些都是无法被修改的了,但是参数是调用 bind 和 call 时的叠加,这是我们唯一可以修改的地方。执行两次 bind 的原理可以参考 bind 的源码,和 call 的差不多,也是目标函数和 this 是被固定的了,只有参数列表会叠加


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马