Skip to content

bind、call、apply 的区别

手动修改 this 的三个方法

显示手动修改 this 可以使用 bind、call、apply 这三个方法,这三个方法都是写在 Object.prototype 上面的,可以直接使用。

apply

apply 接受两个参数,第一个参数是 this 的指向,第二个参数是函数接受的参数,以数组的形式传入

改变 this 指向后原函数会立即执行,且此方法只是临时改变 this 指向一次

javascript
function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); _// this会变成传入的obj,传入的参数必须是一个数组;_
fn(1,2) _// this指向window_

当第一个参数为 nullundefined 的时候,默认指向 window(在浏览器中)

javascript
fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

手写apply

javascript
// 缺点: 容易变量名冲突 fn
Function.prototype.myApply = function(ctx,args){
  
  //保留原有函数
  const fn = this;
  // 在ctx 上添加属性
  ctx.fn = fn;
  // 通过ctx 调用函数, 将obj作为 fn 函数的上下文
 const ret =  ctx.fn(...args);
  // 删除 fn 属性
 delete ctx.fn
 return  ret
  
}
function fun(a,b,c){
  console.log(a,b,c)
  return this.a
}

const obj1 = {a:10};
console.log(fun.myApply(obj1,[1,2,3]))

使用Symbol

javascript

//使用Symbol 防止命名冲突
Function.prototype.myApply2 = function (ctx, arg) {
  // 处理参数为 null 和undefined 情况
   ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
   //保留原有函数
   const fn = this;
   const key = Symbol("apply");
   // 在ctx 上添加属性
   //   Object.defineProperty(ctx,key,{
   //     enumerable:false,
   //     value:this
   //   })
   ctx[key] = fn;
   // 通过ctx 调用函数, 将obj作为 fn 函数的上下文
   const ret = ctx[key](...arg);

   //删除原有属性
   delete ctx[key];
   return ret

 }

const obj = {a:10};
function test(){
  return this;
}
console.log(test.myCall2(obj))

在线地址: 手写apply

image-20230817231008429

call

call 方法的第一个参数也是 this 的指向,后面传入的是一个参数列表

apply 一样,改变 this 指向后原函数会立即执行,且此方法只是临时改变 this 指向一次

javascript
function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.call(obj,1,2); _// this会变成传入的obj,传入的参数必须是一个数组;_
fn(1,2) _// this指向window_

同样的,当第一个参数为 nullundefined 的时候,默认指向 window(在浏览器中)

javascript
fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

手写call 方法:

javascript
Function.prototype.myCall = function(context) {
  let ctx = context || window || global;
  // 修改 this 指向
  ctx.fn = this;
  // 收集参数
  const arr = [];
  for(let i = 1; i < arguments.length; i++) {
    arr.push(arguments[i]);
  }

  const res = eval(`ctx.fn(${arr.join(',')})`);

  delete ctx.fn;
  return res;
}

上面这个代码中使用的收集参数的方法是,通过 arguments 和函数拼接成字符串,然后使用 eval 来执行函数。但是上面的代码中还是存在问题,原因就是原本上面的代码是不打算使用 ES6 的语法,但是使用了 ES6 的地方,修改都是很简单的事情。

上面代码中存在的问题就是,关于将函数绑在传入的参数上的时候,取得名字是一个固定的,而如果原来的对象上存在这个名字的属性,就会对原来的对象造成破坏。

目前可以想到的解决方法就是使用 ES6 引入的 symbol 包装一下。

javascript
Function.prototype.myCall = function(context) {
  let ctx = context || window || global;
  const fn = Symbol('fn');
  ctx[fn] = this;
  // 收集参数
  const arr = [];
  for(let i = 1; i < arguments.length; i++) {
    arr.push(arguments[i]);
  }

  const res = eval(`ctx[fn](${arr.join(',')})`);

  delete ctx[fn];
  return res;
}

使用 ES6 的语法写的话,在参数收集的地方可以换成 ... 运算符:

javascript
Function.prototype.myCall = function(context, ...arg) {
  let ctx = context || window || global;
  const fn = Symbol('fn');
  ctx[fn] = this;

  const res = ctx[fn](...arg);

  delete ctx[fn];
  return res;
}

在线地址:手写call

image-20230817231139957

bind

bind 方法和 call 很相似,第一参数也是 this 的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变 this 指向后不会立即执行,而是返回一个永久改变 this 指向的函数

javascript
function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); _// this 也会变成传入的obj ,bind不是立即执行需要执行一次_
bindFn(1,2) _// this指向obj_
fn(1,2) _// this指向window_

总结

从上面可以看到,applycallbind 三者的区别在于:

  • 三者都可以改变函数的 this 对象指向
  • 三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefinednull,则默认指向全局 window
  • 三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 applycall 是一次性传入参数,而 bind 可以分为多次传入
  • bind 是返回绑定 this 之后的函数,applycall 则是立即执行。

手写 bind

实现 bind 的步骤,我们可以分解成为三部分:

  • 修改 this 指向
  • 动态传递参数
javascript
// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()

// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)
  • 兼容 new 关键字

整体实现代码如下:

javascript
Function.prototype.myBind = function(context,...args){
   console.log(this)
  const fn = this;
  if(!Object.prototype.toString.call(fn) === "[Object function]") return new TypeError("is not function");
  const Fun = function(){
    
    // 如果是通过 new 调用 ,this 是指向 obj 对象;
    const _this = this instanceof Fun ?  this : context
    
    return fn.apply(_this,[...args,...arguments])
  }
  // 改变Fun 的 原型,为this.prototype
  Fun.prototype  = Object.create(fn.prototype)
  return  Fun
}
javascript
const obj = {a:3}
const obj2 = {a:3}
function test(){
  console.log(JSON.stringify(this))
  console.log(this === obj)
}

const test2 = test.bind(obj); // 得到的是Fun 函数
const test3 = test2.bind(obj2);
test2()
test3()
const testObj = new test2()

const test4 = test.myBind(obj); // 得到的是Fun 函数
const test5 = test4.myBind(obj2);
test4()
test5()

在线地址: 手写bindimage-20230817230121505

前端知识体系 · wcrane