深究call和instanceof的原理及其源代码

189 阅读4分钟

前言

本文,便是一段专为探索与驾驭JavaScript类型判断艺术量身定制的奇幻旅程。我们将携手踏入这片既熟悉又时常令人困惑的领域,揭开每种类型背后的秘密面纱,掌握那些在编码征途中不可或缺的“辨形术”。从朴素的typeof咒语,到进阶的instanceof与构造函数比对,再到ES6新晋贵族Array.isArrayObject.prototype.toString.call的精妙运用,每一式每一招都蕴含着转变代码世界的无限可能。

正文

数据类型

原始类型

  • 数值Number
  • 字符串String
  • 布尔值Boolean
  • Null
  • Undefined
  • Symbol
  • BigInt

引用类型

  • 对象Object
  • 函数Function
  • 数组Array
  • Date

定义上面提到的所有数据类型

// 原始数据类型
let s = '123';
let n = 123;
let f = true;
let u = undefined;
let nul = null;
let sy = Symbol(123);
let b = BigInt(123);

// 引用类型
let obj = {};
let arr = [];
let fn = function () {};
let date = new Date();// date类型对象(特殊对象)

typeof

console.log(typeof(s));
console.log(typeof(n));
console.log(typeof(f));
console.log(typeof(u));
console.log(typeof(sy));
console.log(typeof(nul));// object --> 二进制前三位为0

console.log(typeof(obj));// object
console.log(typeof(arr));// object
console.log(typeof(fn));
console.log(typeof(date));// object

image.png

typeof结论

  1. 可以判断除 null 之外的所有原始类型
  2. 除了function其他所有的引用类型都会判断为object
  3. typeof是通过将值转换为二进制后判断其二进制前三位是否为0,是则为object

instanceof

console.log(obj instanceof Object);
console.log(arr instanceof Array);
console.log(obj instanceof Object);
console.log(date instanceof Date);

image.png

console.log(s instanceof String);// false

image.png

面试官叫你手写instanceof

  • 首先我们要知道arr.__proto__ = Array.prototype;Array.prototype.__proto__ = Object.prototype;
  • 一直往上找原型,直到满足=L.__proto__ === R.prototype,同时将L.__proto__赋值给L
function myinstanceof(L, R) {
    while (L !== null) {
      if (L.__proto__ === R.prototype){
        return true;
      }
      L = L.__proto__;
    }

    return false;
}

console.log(myinstanceof([], Array));// true
console.log(myinstanceof([], Object));// true
console.log(myinstanceof({}, Array));// fals

image.png

instanceof结论

  1. 只能判断引用类型
  2. 通过原型链查找来判断类型

Object.prototype.toString.call

Object.prototype.toString(this),该方法只能判断对象类型

var a = {}
var b = new Date();
var c = function(){};
var d = [];
var e = 123;
console.log(Object.prototype.toString(a));
console.log(Object.prototype.toString(b));
console.log(Object.prototype.toString(c));
console.log(Object.prototype.toString(d));
console.log(Object.prototype.toString(e));

image.png

步骤 1.如果 this 值未定义, 返回“[对象未定义]” 2. 如果 此值为 null, 返回“[object Null]” 3. 将 O 作为 ToObject(this) 的执行结果 4. 定义一个class作为内部属性 [[class]] 的值 5. 返回由 "[object" 和 class 和 "]" 组成的字符串

Object.prototype.toString.call(xxx)

用来获取对象类型的标准且可靠的方法,利用了Object.prototype.toString方法,并通过.call()方法改变其内部的this绑定,使其能够应用于任何对象,从而判断该对象的具体类型

var a = {}
var b = new Date();
var c = function(){};
var d = [];
var e = 123;
console.log(Object.prototype.toString.call(a));
console.log(Object.prototype.toString.call(b));
console.log(Object.prototype.toString.call(c));
console.log(Object.prototype.toString.call(d));
console.log(Object.prototype.toString.call(e));

image.png

手写call方法(改变任意函数的调用上下文(即函数内部的this值))

  • 类型检查: 首先,通过typeof this !== 'function'检查调用mycall方法的主体(即this指向的对象)是否为函数类型。如果不是函数,则抛出一个类型错误(TypeError),提示调用者这不是一个函数。
  • 创建唯一标识符: 使用Symbol创建一个独一无二的属性名fn。这样做可以防止与对象中已存在的属性名冲突,保证临时添加的函数不会覆盖原有的属性。
  • 将函数绑定到目标对象: 将当前函数(this,即想要改变调用上下文的函数)赋值给context对象上的fn属性。这样,context对象就暂时拥有了这个函数,可以通过context[fn]来调用它。
  • 调用函数并利用隐式绑定: 接下来,通过context[fn]()执行这个函数。由于是在context对象上调用的,根据JavaScript的函数调用规则,这里的this会被绑定到context上,实现了上下文的改变。
  • 清理: 最后,通过delete context[fn]删除之前添加到context对象上的临时函数,以保持原对象的清洁,不留下任何副作用。
Function.prototype.mycall = function(context) {
    // 调用的哥们是不是函数体
    if (typeof this !== 'function' ) {
        return new TypeError(this + 'is not a function');
    }

    // this里面的this => context
    const fn = Symbol('key');
    context[fn] = this;// 让对象拥有该函数 {Symbol('key'): foo}
    context[fn]();// 触发了隐式绑定
    delete context[fn];
}

应用

var obj = {
    a:1
}
function foo() {
    console.log(this.a);
}


foo.mycall(obj);

image.png

Array.isArray

判断某个变量是否为数组类型的一个内置函数

let a = [];
let b = {};
let c = 123;
console.log(Array.isArray(a));
console.log(Array.isArray(b));
console.log(Array.isArray(c));

image.png