《不一样的javascript》系列
文章目录
前言
上一篇讲this指向时候讲到四大绑定时说到:call…这三个可以函数显示绑定一个对象到一个函数的this上,这篇主要讲一下三者区别以及手写他们。至于你问为啥要手写它呢,别问,问就是在某个面试上遇到过 。:)
提示:以下是本篇文章正文内容,下面案例可供参考
一、call、apply、bind是什么,有何用,区别是什么?
1. 语法
2. 相同点
- 三个的作用都是把一个对象绑定到一个执行函数的this上(运行时函数才有this)
- 如果第一个参数是null,undefined 非严格模式转换为window对象,严格模式为null,undefined
- 如果第一个参数是数字或者字符串等非对象,在非严格模式下转为对应的字面量对象,严格模式不会。
- 第一个参数没有 非严格模式 window 严格模式undefined
// 'use strict'
var obj = {
a: 1
}
function person() {
console.log(this);
}
var a = 3;
person.call()//window
person.call(obj) //obj
person.call(null) //window strict null
person.call(undefined) //window strict undefined
person.call('11') //String{"11"} strict '11'
person.call(11) //Number{11} strict 11
person.apply(null) //window
person.apply(undefined) //window
person.apply(11) //Number{11}
var fn = person.bind(null) //window
fn()
var fn2 = person.bind(undefined) //window
fn2()
var fn3 = person.bind(11) //Number{11}
fn3()
3. 不同点
- bind 与call,apply 的区别是bind不会执行该函数,只改变this指向,其余参数不做要求。
- call apply 都执行该函数,区别在于apply 除了第一个参数 剩余参数只能是一个数组或者类数组里面包含若干参数
- 而call为若干个参数列表
4. 疑惑点
- 上面说bind 在非严格模式下null,undefined转换window 但是在MDN文档中
调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
与我下面测试不符,有知道可告知吾。
function person() {
console.log(this);
}
var obj2 = {
fn4: function() {
console.log(this);//obj2
setTimeout(() => {
var fn6 = person.bind(null);
fn6() //window
}, 100)
}
}
obj2.fn4()
5. 用途
这个就有点多了,略
二、手写
有了上面的准备开始写吧,还记得上一篇隐式绑定吗?
1. apply
/*
* 不做函数监测的判断
* 实现apply的类型功能
* 利用隐式绑定
* 非严格模式
* 不判断类数组
*/
Function.prototype.myApply = function(context, args) {
args = args ? args : []//参数为空
if (context === null || context === undefined) {
context = window
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
} else {
context = Object(context)
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
var fn = this //this就是你要改变的函数
const key = Symbol() //给context新增一个独一无二的属性以免覆盖原有属性
context[key] = fn //相当于context 增加fn方法,让context拥有这个函数
const res = context[key](...args)
//通过隐式绑定的方式调用函数 相当于obj.fn() , ...args es6语法
delete context[key] //删除添加的属性
console.log(context[key]);
return res //返回函数调用的返回值
}
var obj = {
a: 2
}
var a = 3;
var fn = function() {
console.log(this);
}
fn.myApply(obj) //obj
fn.myApply(null) //window
fn.myApply(11) //Number{11}
2. call
唯一区别第一行,注意两个…的区别
Function.prototype.mybind = function(context, ...args) {
args = args ? args : []
if (context === null || context === undefined) {
context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
} else {
context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
var fn = this //就是你要改变的函数
const key = Symbol() //给context新增一个独一无二的属性以免覆盖原有属性
context[key] = fn //相当于context 增加fn方法,让context拥有这个函数
const res = context[key](...args) //通过隐式绑定的方式调用函数 相当于obj.fn() , ...args es6语法把类数组转化为数组
delete context[key] //删除添加的属性
console.log(context[key]);
return res //返回函数调用的返回值
}
function testRest(...args) {
//rest运算,剩余运算符(the rest operator)用于解构数组和对象
console.log(args); //[1,2,3,4]
console.log(...args);
//1,2,3,4扩展运算符:拆解数组,将一个数组转为用逗号分隔的参数序列
}
testRest(1, 2, 3, 4)
3. bind
- 这里注意一点就是bind是可以new的,new的优先级是大于bind的
- 有的教程里面把不绑定new 的对象,这与优先级违背了。
function person() {
console.log(this); //person
}
var a = 3
var obj = {
a: 2
}
var newPerson = person.bind(obj)
var np = new newPerson();
console.log(np); //person new优先级
var newFn = person.bind(obj);
newFn() //{a:2}
Function.prototype.myBind = function(context, ...args) {
const fn = this
args = args ? args : []
return function newFn(...secondArgs) { //二次传参问题
if (this instanceof newFn) { //解决new的问题
return new fn(...args, ...secondArgs)//如果是new 就返回一个对象
}
return fn.apply(context, [...args, ...secondArgs])
}
}
var newPerson2 = person.myBind(obj)
var np2 = new newPerson2();
console.log(np2); //person new优先级
var newFn2 = person.myBind(obj);
newFn2() //{a:2}
4. 总结一下
- 核心:利用隐式绑定。
- 上面的代码只是实现了一部分,距离真正的实现还远着。
- 上面代码参考与(我这小脑瓜子可实现不了,侵删)
https://juejin.cn/post/6844903891092389901#heading-12
https://juejin.cn/post/6844903906279964686#heading-22
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/
三、做道题
function fn() {
console.log(this)
}
var obj2 = {
name: '233'
}
fn.call(obj2)
fn.call(fn)
fn.call.call(function() {
console.log(this, 1)
})
fn.call.call.call.call(function() {
console.log(this)
}, obj2)
会输出什么呢?
你可能觉得很乱
那我换一下
function fn() {
console.log(this)
}
var obj2 = {
name: '233'
}
function fn3() {
console.log(this, 'fn3');
}
console.log(fn.prototype);
fn.call(obj2) //obj2
fn.call(fn)
fn.call.call(fn3) //window fn3
fn.call.call.call.call(fn3, obj2) // obj fn3
先说个结论:
- fn.call.call.call.call.call…call(fn3)
结果fn3()//window - fn.call.call.call.call.call…call(fn3,obj)
结果fn3()//obj
原型链不懂得可以看一下这
- 第一点 fn.call.call.call.call会是什么
你可以console.log(fn.prototype);
里面找call然后在找call 一直找下去 - 第二点 Function.prototype.call意味什么?就是意味着只要是函数都有call ,call也是函数它也有call,第一点可以看到
- 第三点 fn.call…最终起作用得只有最后两个call ,fn已经没用了,最后就是call.call()如下面。
- 把fn 比作对象(它也确实是一个对象,以原型链来说)fn.call…相当于一个对象里一个子属性对象得子属性对象…
//这里改为对象会好点
// function fx() {
// var fx = function() {
// var fx = function() {
// console.log('1');
// }
// }
// }
// console.log(fx.fx.fx());
第四点:如我们上面手写一样,fn.call(context,args) 内部是隐式绑定conext.fn(args)
所以
- fn.call.call…call = calll
- fn.call.call(fn3) = call.call(fn3)
- 内部就是 context => fn3 fn=>call
- conext.fn(args) => fn3.call(args) args为空绑定到window
- fn.call.call(fn3,obj) => fn3.call(obj) obj绑定到fn3上
总结:
call数>=2 看最后call参数 call(fn3,obj)
- 只有fn3 则执行fn3() 绑定window到fn3上
- fn3,obj 则执行fn3() 绑定obj到fn3上