手写源码系列目录
一、作用
call
、
apply
、
bind
作用是改变函数执行时的上下文,简而言之就是改变函数运行时的
this
指向
那么什么情况下需要改变
this
的指向呢?下面举个例子
var name ="lucy";var obj ={name:"martin",say:function(){
console.log(this.name);}};
obj.say();// martin,this 指向 obj 对象setTimeout(obj.say,0);// lucy,this 指向 window 对象
从上面可以看到,正常情况
say
方法输出
martin
但是我们把
say
放在
setTimeout
方法中,在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,这时候
this
指向
window
,所以输出
lucy
我们实际需要的是
this
指向
obj
对象,这时候就需要该改变
this
指向了
setTimeout(obj.say.bind(obj),0);//martin,this指向obj对象
二、手写call方法
**
call
做了什么:**
- 将函数设为对象的属性
- 执行和删除这个函数
- 指定
this
到函数并传入给定参数执行函数 - 如果不传入参数,默认指向
window
**分析:如何在函数执行时绑定
this
**
- 如
var obj = {x:100,fn() { this.x }}
- 执行
obj.fn()
,此时fn
内部的this
就指向了obj
可借此来实现函数绑定this
- 原生
call
、apply
传入的this如果是值类型,会被new Object
(如fn.call('abc'))
//实现call方法// 相当于在obj上调用fn方法,this指向obj // var obj = {fn: function(){console.log(this)}}// obj.fn() fn内部的this指向obj// call就是模拟了这个过程// context 相当于objFunction.prototype.myCall=function(context = window,...args){if(typeof context !=='object') context =newObject(context)// 值类型,变为对象// args 传递过来的参数// this 表示调用call的函数fn// context 是call传入的this// 在context上加一个唯一值,不会出现属性名称的覆盖let fnKey =Symbol()// 相等于 obj[fnKey] = fn
context[fnKey]=this;// this 就是当前的函数// 绑定了thislet result = context[fnKey](...args);// 相当于 obj.fn()执行 fn内部this指向context(obj)// 清理掉 fn ,防止污染(即清掉obj上的fnKey属性)delete context[fnKey];// 返回结果 return result;};
调用:
//用法:f.call(this,arg1)functionf(a,b){
console.log(a+b)
console.log(this.name)}let obj={name:1}
f.myCall(obj,1,2)// 不传obj,this指向window
三、手写apply方法
思路: 利用
this
的上下文特性。
apply
其实就是改一下
参数
的问题
Function.prototype.myApply=function(context = window, args){// 这里传参和call传参不一样if(typeof context !=='object') context =newObject(context)// 值类型,变为对象// args 传递过来的参数// this 表示调用call的函数// context 是apply传入的this// 在context上加一个唯一值,不会出现属性名称的覆盖let fnKey =Symbol()
context[fnKey]=this;// this 就是当前的函数// 绑定了thislet result = context[fnKey](...args);// 清理掉 fn ,防止污染delete context[fnKey];// 返回结果return result;}
调用:
// 使用functionf(a,b){
console.log(a,b)
console.log(this.name)}let obj={name:'张三'}
f.myApply(obj,[1,2])
四、手写bind方法
bind
的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似
函数柯里化
),因为
bind
需要返回一个函数,需要判断一些边界问题,以下是
bind
的实现:
bind
返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new
的方式,我们先来说直接调用的方式- 对于直接调用来说,这里选择了
apply
的方式实现,但是对于参数需要注意以下情况:因为bind
可以实现类似这样的代码f.bind(obj, 1)(2)
,所以我们需要将两边的参数拼接起来 - 最后来说通过
new
的方式,对于new
的情况来说,不会被任何方式改变this
,所以对于这种情况我们需要忽略传入的this
- 箭头函数的底层是
bind
,无法改变this
,只能改变参数
简洁版本:
对于普通函数,绑定
this
指向
对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind=function(context = window,...args){// context 是 bind 传入的 this// args 是 bind 传入的各个参数// this表示调用bind的函数let self =this;// fn.bind(obj) self就是fn//返回了一个函数,...innerArgs为实际调用时传入的参数letfBound=function(...innerArgs){//this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值// 当作为普通函数时,this 默认指向 window,此时结果为 false,将绑定函数的 this 指向 contextreturnself.apply(// 函数执行thisinstanceoffBound?this: context,
args.concat(innerArgs)// 拼接参数);}// 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失// 实现继承的方式: 使用Object.create
fBound.prototype = Object.create(this.prototype);return fBound;}
调用:
// 测试用例functionPerson(name, age){
console.log('Person name:', name);
console.log('Person age:', age);
console.log('Person this:',this);// 构造函数this指向实例对象}// 构造函数原型的方法Person.prototype.say=function(){
console.log('person say');}// 普通函数functionnormalFun(name, age){
console.log('普通函数 name:', name);
console.log('普通函数 age:', age);
console.log('普通函数 this:',this);// 普通函数this指向绑定bind的第一个参数 也就是例子中的obj}var obj ={name:'poetries',age:18}// 先测试作为构造函数调用var bindFun = Person.myBind(obj,'poetry1')// undefinedvar a =newbindFun(10)// Person name: poetry1、Person age: 10、Person this: fBound {}
a.say()// person say// 再测试作为普通函数调用var bindNormalFun = normalFun.myBind(obj,'poetry2')// undefinedbindNormalFun(12)// 普通函数name: poetry2 // 普通函数 age: 12 // 普通函数 this: {name: 'poetries', age: 18}
注意:
bind
之后不能再次修改
this
的指向(箭头函数的底层实现原理依赖
bind
绑定
thi
s后不能再次修改
this
的特性),
bind
多次后执行,函数
this
还是指向第一次
bind
的对象
五、三者区别
apply
apply
接受两个参数,第一个参数是
this
的指向,第二个参数是函数接受的参数,以数组的形式传入
改变
this
指向后原函数会立即执行,且此方法只是临时改变
this
指向一次
functionfn(...args){
console.log(this,args);}let obj ={myname:"张三"}fn.apply(obj,[1,2]);// this会变成传入的obj,传入的参数必须是一个数组;fn(1,2)// this指向window
当第一个参数为
null
、
undefined
的时候,默认指向
window
(在浏览器中)
fn.apply(null,[1,2]);// this指向windowfn.apply(undefined,[1,2]);// this指向window
call
call
方法的第一个参数也是
this
的指向,后面传入的是一个参数列表
跟
apply
一样,改变
this
指向后原函数会立即执行,且此方法只是临时改变
this
指向一次
functionfn(...args){
console.log(this,args);}let obj ={myname:"张三"}fn.call(obj,1,2);// this会变成传入的obj,传入的参数必须是一个数组;fn(1,2)// this指向window
同样的,当第一个参数为
null
、
undefined
的时候,默认指向
window
(在浏览器中)
fn.call(null,[1,2]);// this指向windowfn.call(undefined,[1,2]);// this指向window
bind
bind方法和call很相似,第一参数也是
this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变
this
指向后不会立即执行,而是返回一个永久改变
this
指向的函数
functionfn(...args){
console.log(this,args);}let obj ={myname:"张三"}const bindFn =fn.bind(obj);// this 也会变成传入的obj ,bind不是立即执行需要执行一次bindFn(1,2)// this指向objfn(1,2)// this指向window
小结
从上面可以看到,
apply
、
call
、
bind
三者的区别在于:
- 三者都可以改变函数的
this
对象指向 - 三者第一个参数都是
this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
版权归原作者 真的很上进 所有, 如有侵权,请联系我们删除。