关于 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


目录
相关文章
|
前端开发 安全 UED
【项目实战】从终端到浏览器:实现 ANSI 字体在前端页面的彩色展示
在学习和工作中,我们经常需要使用日志来记录程序的运行状态和调试信息。而为了更好地区分不同的日志等级,我们可以使用不同的颜色来呈现,使其更加醒目和易于阅读。 在下图运行结果中,我们使用了 colorlog 库来实现彩色日志输出。通过定义不同日志等级对应的颜色,我们可以在控制台中以彩色的方式显示日志信息。例如,DEBUG 级别的日志使用白色,INFO 级别的日志使用绿色,WARNING 级别的日志使用黄色,ERROR 级别的日志使用红色,CRITICAL 级别的日志使用蓝色。
|
10月前
|
弹性计算 Linux 数据安全/隐私保护
阿里云服务器最新购买流程与试用流程参考,购买与试用图文教程和注意事项
如何购买和试用阿里云服务器,教程参考来了。阿里云服务器分为免费版和收费版,新用户可免费领取一台云服务器作为试用,轻量应用服务器2核2G目前38元1年,云服务器ECS2核2G3M的价格为99元1年,2核4G5M配置199元1年,下面小编来介绍一下2025年我们购买和试用阿里云服务器的详细流程,以图文形式展示给大家,适合新手用户参考。
阿里云服务器最新购买流程与试用流程参考,购买与试用图文教程和注意事项
|
程序员 开发者
黑马程序员 苍穹外卖项目 Day微信支付问题解决与生成订单号超出上限问题
黑马程序员 苍穹外卖项目 Day微信支付问题解决与生成订单号超出上限问题
612 5
|
域名解析 缓存 网络协议
域名系统DNS_基础知识
域名系统(DNS)使我们能够通过易记的域名访问互联网资源,而非直接使用IP地址。DNS采用层次树状结构,由多个分量组成,如顶级域名(如.com或.cn)位于最右侧。域名长度限制为255个字符,各级域名由相应管理机构监管,顶级域名由ICANN管理。DNS分为国家顶级域名、通用顶级域名和反向域等。域名解析涉及根域名、顶级域名及权限域名服务器,通过递归和迭代查询完成。为提高效率,DNS使用分布式服务器和高速缓存技术。
1091 8
|
网络协议 Ubuntu Linux
在Linux中,如何将本地80端口的请求转发到8080端口,当前主机IP为192.168.16.1,其中本地网卡eth0。
在Linux中,如何将本地80端口的请求转发到8080端口,当前主机IP为192.168.16.1,其中本地网卡eth0。
|
新能源
从零开始做逆变器系列文章之逆变原理
从零开始做逆变器系列文章之逆变原理
从零开始做逆变器系列文章之逆变原理
|
JSON JavaScript 前端开发
Vue中的axios深度探索:从基础安装到高级功能应用的全面指南
在Vue项目中,高效的前后端通信是构建丰富用户体验的关键。axios作为前端与后端沟通的桥梁,其重要性不言而喻。本文将带您领略axios的魅力,从基本概念、安装方法,到高级应用技巧,助您快速掌握在Vue中利用axios进行HTTP请求的精髓。我们不仅会探讨axios的基础用法,如GET、POST请求,还将深入探索跨域配置、全局注册以及设置拦截器等高级功能,助您轻松实现优雅的前后端通信。
|
Swift 图形学 iOS开发
【Swift开发专栏】Swift中的自定义视图与绘制
【4月更文挑战第30天】本文探讨了Swift中自定义视图的创建与绘制,分为基础知识、绘制步骤和性能优化三部分。开发者通过继承`UIView`,重写`draw(_:)`方法并利用Core Graphics进行2D绘图。提高性能的技巧包括避免重复绘制、使用轻量级视图、优化图形上下文使用、启用图层背板及避免阻塞主线程。自定义视图让iOS和macOS界面更独特高效,进阶可探索Core Animation和Metal等技术。
233 0
|
网络协议 算法 5G
TCP 拥塞控制详解 | 7. 超越 TCP(下)
TCP 拥塞控制详解 | 7. 超越 TCP(下)
831 1
TCP 拥塞控制详解 | 7. 超越 TCP(下)
|
消息中间件 监控 Dubbo
【SpringBoot学习笔记 十四】SpringBoot+Dubbo+Zookeeper集成开发
【SpringBoot学习笔记 十四】SpringBoot+Dubbo+Zookeeper集成开发
557 0