typeof

利用typeof判定类型的取值范围是:’undefined’ /‘boolean’ /‘string’ /‘number’ /‘object’ /‘function’ /‘symbol’

在JavaScript内部使用typeof判断类型依据的是二进制,根据变量的机器码低位1-3位存储其类型信息,有如下规则:

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数

这里就引出了JavaScript的经典bug typeof null === 'object' // true

因为null的机器码为全0,那自然前三位也是0,所以这里会误判null的类型为object

instanceof

instanceof用来判断实例是否属于某种类型,或者实例的祖先属不属于某种类型,翻译一下就是只要右边变量的 prototype 在左边变量的原型链上即可,大致过程如下

function intanceofSelf(son, parent) {
  if(son === null || parent === null) {
    return false
  }
  let proto = son.__proto__
  while(1) {
    if(proto === parent.prototype) {
      return true
    }
    if(proto === null) {
      return false
    }
    proto = proto.__proto__
  }
}
console.log(intanceofSelf([], Array));
console.log([] instanceof Array);

执行结果如下

QQ截图20210406170900

贴一张图来回忆一下原型

163a55d5d35b866d

new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

来看一下真正的new操作之后是什么效果

image-20210407220837178

我们班来简单梳理一下new操作符所做的事情:

  1. 创建一个对象
  2. 将构造函数的原型挂载到新对象上
  3. 执行构造函数
  4. 返回新对象

大体功能已经分析出来了,开始代码实现,由于我们无法实现关键字的形式,我们就以工厂函数的形式来实现new操作

function createNewObj(constructor) {
  const args = Array.prototype.slice.call(arguments, 1)

  const obj = {}
  obj.__proto__ = constructor.prototype
  constructor.apply(obj, args)

  return obj
}

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.getAge = function () {
  return this.age
}

let p = createNewObj(Person, 'king', 17)
console.log(p)

来看一下执行结果,与原生new的效果相同,大功告成

image-20210407221716441

call

call是再Function原型上的方法,它的作用是改变函数执行时的this指向,也就是this绑定规则中的显示绑定

先来看一下使用示例,理解了使用方法才能去实现它

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

实现思路:

  1. 将调用方法添加到指定的this上
  2. 执行方法
  3. 删除指定this上的方法

PS:这里注意call的this指向可以传null,当传值是null的时候,this指向window对象

Function.prototype.callSelf = function(ctx) {
  ctx = ctx || window
  const args = Array.from(arguments)
  args.shift()
  ctx.fun = this
  ctx.fun(...args)
  delete ctx.fun
}
const bar = {
  age: 18
}
var age = 17
function foo(name) {
  console.log(name)
  console.log(this.age)
}
foo.callSelf(bar, '张三')
foo.callSelf(null, '张三')
foo('张三')

来看一下运行结果(不可以在node环境运行,node环境没有window)

image-20210407174803830

可以看到传值为null的时候也实现了绑定为window

PS:这只是原理上实现,call在ES3就已经实现了,我在代码中使用了多个ES6的方法

apply

apply的作用于call一样,唯一的区别在于二者接收的参数,call的参数是this指向+函数参数,apply的参数是this指向+函数参数组成的数组,如foo.call(bar, arg1, arg2)foo.apply(bar, [arg1, arg2])

那么这里也不再细说apply的原理了,直接上代码

Function.prototype.applySelf = function(ctx, args) {
  ctx = ctx || window
  ctx.fun = this
  ctx.fun(...args)
  delete ctx.fun
}
const bar = {
  age: 18
}
var age = 17
function foo(name) {
  console.log(name)
  console.log(this.age)
}

foo.applySelf(bar, ['张三'])
foo.applySelf(null, ['张三'])
foo('张三')

运行结果

image-20210407202757127

bind

bind与apply和call作用大抵相同,区别在于bind返回的是一个可执行函数,看一下MDN上bind的介绍

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

那也就是说,bind不再只是this指向的改变,而且要兼顾原型等因素,我们一步一步来

先来实现返回指定this的函数

Function.prototype.bindSelf = function (ctx) {
  var _this = this
  return function () {
    return _this.apply(ctx)
  }
}

var bar = {
  age: 18
}
var age = 19

function foo() {
  console.log(this.age)
}

const f = foo.bindSelf(bar)
f()
foo()

不要忘了还可以接受参数,这里不再是简单的参数展开,他还可以分批传入(柯里化),如下

image-20210408083211406

当然,也不需要太过担心,其实实现也很简单,从两个函数作用域的arguments中提取即可

Function.prototype.bindSelf = function(ctx) {
  var _this = this
  var args = Array.prototype.slice.call(arguments, 1)
  return function() {
    _this.apply(ctx, args.concat(Array.from(arguments)))
  }
}

var bar = {
  value: 1
}
function foo(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
const f = foo.bindSelf(bar, 'king')
f(18)

传参的问题解决了,接下来就是最头疼的构造器效果了,我们先来new一下试试,看看缺了什么

const f = foo.bindSelf(bar, 'king')
const o = new f(17)
console.log(o)
// 17
// 1
// {}
const f2 = foo.bind(bar, 'king')
const o2 = new f2(17)
console.log(o2)
// 17
// undefined
// foo {}
//      foo的实例对象

如果你从上面看下来你会知道在执行new操作符的时候会将this指向要创建出来的对象实例,所以原生bind返回的函数在实例化时会找不到value,而我们自定义的bind在执行new操作符的时候this仍然指向bar对象,而且实例的原型链也有问题,所以会分析不出对象实例的类型

知道了问题所在我们就开始继续优化

Function.prototype.bindSelf = function(ctx) {
  var _this = this
  console.log(_this);
  var args = Array.prototype.slice.call(arguments, 1)
  var midFun = function() {
    _this.apply(this instanceof midFun ? this : ctx, args.concat(Array.from(arguments)))
  }
  midFun.prototype = this.prototype
  return midFun
}

这里我们通过instanceof来判断当前操作是new还是作为普通函数执行。当执行new操作时,this指向对象实例,instanceof的结果为true,保持this指向对象实例;否则就是作为普通函数执行,this指向一开始传进来的上下文。这一段要好好消化一下,比较绕。

再来执行上面的测试数据,大功告成

QQ图片20210408191414


前端小白