关于 this 指向、如何实现 new call apply bind 我所知道的

简介: 关于 this 指向、如何实现 new call apply bind 我所知道的

image.png


持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 7 天,点击查看活动详情

关键词: this指向 call apply bind new

为什么要串起来讲?因为这几个知识点是一起的。相互印证之下更好理解。


this 是什么?


this 是指针,指向调用函数的对象。决定 this 指向的是函数的调用方式。


如何改变 this 指向?


首先必须了解到为什么要改变 this 指向:隐式传递对象的引用,而不是通过传参的方式。

如果不利用 this,我们会如何在不同的上下文对象中重复使用函数?

function fn(ctx) {
  return ctx.name
}
function operate(ctx) {
  console.log('hello ' + fn(ctx))
}
var a = {
  name: 'a'
}
fn(a) // a
operate(a) // hello a
// 使用 this
var b = {
  name: 'b',
  operate: function () {
    operate(this)
  }
}
// 隐式传递对象的引用
b.operate() // hello b


然后我们需要了解到:this 是在函数被调用时发生绑定,所以决定 this 指向的是函数的调用方式。

this 绑定规则有四种(函数的调用方式):

  • 默认绑定。作为函数,默认绑定在全局变量 window 下。
  • 隐式绑定。作为方法,关联在一个对象上obj.fn
  • 显式绑定。通过 call/apply/bind 显式绑定,指向绑定的对象。
  • new 绑定。作为构造函数,实例化一个对象。。
  • 需要注意:箭头函数没有自己的 this,继承于上一层上下文的 this(与声明所在的上下文相同)。

运行环境只针对浏览器且非严格模式。

// 默认绑定 - 函数挂在 window 下,实际是 window.fn()
var name = 'global name';
function fn(name) {
  if (name) {
    this.name = name
  } else {
    console.log(this.name);
  }
}
fn() // global name
// 隐式绑定
var p = {
  name: 'p name',
  fn
}
p.fn() // p name
// 显式绑定
var newFn = p.fn;
newFn() // global name
newFn.apply(p) // p name
// new 绑定 -> 可以先看后文 如何实现 new
var fn1 = new fn('new name')
console.log(fn1.name) // new name
// 箭头函数是例外
var b = {
  name: 'b name',
  fn: () => {
    console.log(this.name)
  }
}
b.fn() // global name

通过手写代码加深理解

如何实现 call


call 实现的关键在于隐式改变 this 的指向。

实现要点:

  1. 如果不传入参数或者参数为 null,默认指向为 window,值为原始值的指向该原始值的自动包装对象,如 StringNumberBoolean
  2. 为了避免函数名与上下文(context)的属性发生冲突,使用 Symbol 类型作为唯一值
  3. 将函数作为传入的上下文(context)属性执行
  4. 函数执行完成后删除该属性
  5. 返回执行结果
/**
 * 1. 将函数设为传入参数的属性
 * 2. 指定 this 到函数并传入给定参数执行函数
 * 3. 如果不传入参数或者参数为 null,默认指向为 window 
 * 4. 删除参数上的函数
 */
Function.prototype.myCall = function (context, ...args) {
  let cxt = context || window;
  // 将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this)
  // 新建一个唯一的Symbol变量避免重复
  let func = Symbol()
  cxt[func] = this;
  args = args ? args : []
  // 以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向
  const res = args.length > 0 ? cxt[func](...args) : cxt[func]();
  // 删除该方法,不然会对传入对象造成污染(添加该方法)
  delete cxt[func];
  return res;
}

测试代码:

// test code
const foo = {
  name: 'kane'
}
const name = 'logger';
function bar(job, age) {
  console.log(this.name);
  console.log(job, age);
}
bar.myCall(foo, 'sb', 20);
bar.myCall(null, 'aho', 25);

如何实现 apply


apply 实现原理与 call 相同,差别在于参数的处理和判断

实现要点:

  1. this 可能传入 null,第二个参数可以不传,但类型必须为数组或者类数组 其他的则与 call 相同
  2. 如果不传入参数或者参数为 null,默认指向为 window,值为原始值的指向该原始值的自动包装对象,如 StringNumberBoolean
  3. 为了避免函数名与上下文(context)的属性发生冲突,使用 Symbol 类型作为唯一值
  4. 将函数作为传入的上下文(context)属性执行
  5. 函数执行完成后删除该属性
  6. 返回执行结果
/**
 * 第二个参数可以不传,但类型必须为数组或者类数组
 */
Function.prototype.myApply = function (context, args = []) {
  let cxt = context || window;
  // 将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this)
  // 新建一个唯一的Symbol变量避免重复
  let func = Symbol()
  cxt[func] = this;
  // 以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向
  const res = args.length > 0 ? cxt[func](...args) : cxt[func]();
  delete cxt[func];
  return res;
}


测试代码


const foo = {
  name: 'Selina'
}
const name = 'Chirs';
function bar(job, age) {
  console.log(this.name);
  console.log(job, age);
}
bar.myApply(foo, ['programmer', 20]);
bar.myApply(null, ['teacher', 25]);

如何实现 bind

bind 在此基础上,增加了一些业务判断。整体实现较为复杂,我们可以分步骤来分析。

step 1: 绑定原型

Function.prototype.myBind = function() {}

step 2: 改变 this 指向

Function.prototype.myBind = function(target) {
  const _this = this;
  return function() {
    _this.apply(target)
  } 
}

step 3: 支持柯里化

柯里化举例


function fn(x) {
 return function (y) {
  return x + y;
 }
}
var fn1 = fn(1)(2);
fn1(3) // 6


柯里化使用了闭包,当执行 fn1 的时候,形成了闭包,函数内获取到了外层函数的 x

实现步骤:

  1. 获取当前外部函数的 arguments, 去除绑定的对象,保存成变量 args.
  2. return -> 再一次获取当前函数的 arguments, 最终用 finalArgs 进行合并。
Function.prototype.myBind = function(target) {
  const _this = this;
  const args = [...arguments].slice(1)
  return function (){
    const finalArgs = [...args, ...arguments]
    _this.apply(target, finalArgs)
  }
}

step 4: new 的调用


通过 bind 绑定之后,依然是可以通过 new 来进行实例化的, new 的优先级会高于 bindnew 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(设置该对象的 constructor )到另一个对象 ;
  3. 将步骤1 新创建的对象作为 this 的上下文 ;
  4. 如果该函数没有返回对象,则返回 this
Function.prototype.myBind = function(target) {
  const _this = this;
  const args = [...arguments].slice(1)
  return function (){
    const finalArgs = [...args, ...arguments];
    if(new.target !== undefined) { // new.target 用来检测是否是被 new 调用
      const result = _this.apply(target, finalArgs);
      if(result instanceof Object) { // 判断改函数是否返回对象
        return reuslt;
      }
      return this // 没有返回对象就返回 this
    }else { // 不是 new
      _this.apply(target, finalArgs)
    }
  }
}

step 5: 保留函数原型

Function.prototype.myBind = function (target) {
  // 判断是否为函数调用
  if (typeof target !== 'function' || Object.prototype.toString.call(target) !== '[object Function]') {
    throw new TypeError(this + ' must be a function');
  }
  const _this = this;
  const args = [...arguments].slice(1)
  let wrapper;
  const binder = function () {
    const finalArgs = [...args, ...arguments];
    if (new.target !== undefined) {
      const result = _this.apply(target, finalArgs);
      if (result instanceof Object) return reuslt;
      return this
    } else {
      _this.apply(target, finalArgs)
    }
  }
  const wrapperLength = Math.max(0, _this.length - args.length);
  const wrapperArgs = [];
  for (var i = 0; i < wrapperLength; i++) {
    wrapperArgs.push('$' + i);
  }
  wrapper = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);
  if (_this.prototype) {
    // wrapper.prototype = _this.prototype 
    // _this.prototype 导致原函数的原型被修改 应使用 Object.create
    wrapper.prototype = Object.create(_this.prototype);
    wrapper.prototype.constructor = _this;
  }
  return wrapper
}

如何实现 new


new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。

  1. 创建一个新对象
  2. 这个新对象会被执行 __proto__ 原型链接
  3. 将构造函数的作用域赋值给新对象,即 this 指向这个新对象
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function myNew() {
  var obj = new Object() // 创建一个新对象
  Constructor = [].shift.call(arguments);
  obj.__proto__ = Constructor.prototype; // 创建一个新对象
  var ret = Constructor.apply(obj, arguments); // //将构造函数绑定到obj中
  // ret || obj 这里这么写,考虑了构造函数显示返回 null 的情况
  return typeof ret === 'object' ? ret || obj : obj;
};
function person(name, age) {
  this.name = name
  this.age = age
}
let p = myNew(person, '布兰', 12)
console.log(p)  // { name: '布兰', age: 12 }

参考资料:

《你不知道的 JavaScript》

《JavaScript 忍者秘籍》

Function.prototype.bind

function-bind


目录
相关文章
|
1月前
|
JavaScript 前端开发 开发者
call、bind、apply区别
【10月更文挑战第26天】`call`、`bind` 和 `apply` 方法在改变函数 `this` 指向和参数传递方面各有特点,开发者可以根据具体的需求和使用场景选择合适的方法来实现更灵活和高效的JavaScript编程。
32 1
|
JavaScript 前端开发
面试官: call、apply和 bind有什么区别?
面试官: call、apply和 bind有什么区别?
|
7月前
|
JavaScript
JS中call()、apply()、bind()改变this指向的原理
JS中call()、apply()、bind()改变this指向的原理
bind、call、apply 三者之间区别?如何实现一个bind?
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
bind、call、apply 区别
bind、call、apply 区别
82 0
call、apply、bind笔记
call、apply、bind笔记
65 0
|
前端开发
前端学习案例1:apply,call,bind使用1
前端学习案例1:apply,call,bind使用1
85 0
前端学习案例1:apply,call,bind使用1
|
前端开发
前端学习案例2:apply,call,bind使用2
前端学习案例2:apply,call,bind使用2
76 0
前端学习案例2:apply,call,bind使用2