js原型链和变量类型检测的方式和粒度

先来谈一谈js的原型链

先请记住以下几个原则

  • 1、对象内部具有一个 [[Prototype]] 属性,该属性不可直接访问,可以通过 __proto__ 属性 (这是一个不是w3c规范但是所有浏览器都实现了的属性,所以不建议使用这种方法)和函数式接口Object.getPrototypeOf() | Reflect.getPrototypeOf() 来读取该内部属性,最重要的是,该属性指向创建本对象的原型对象。
  • 2、js的函数既是函数也是对象(可能念起来有点拗口),每个函数都有一个prototype属性指向堆中的一个属于该函数的原型对象,每个原型对象内部都有一个独有属性constructor,指向该原型对象的构造函数。
  • 3、原型对象也是对象,因此它也有自己的__proto__属性,最终会指向Object.prototype。

让我们根据上门三条原则来画一个原型链流程图(不要在意画的丑这个小细节):
js原型链和变量类型检测的方式和粒度_第1张图片
可能大家比较疑惑的是Object.__proto__Function.__proto__同时指向了Function.prototype,首先Object和Fucntion都是一个函数类型,既然是一个函数那么它就是一个Function实例,由new Function([native code])生成,所以它们的__proto__自然是指向生成他们的构造函数的prototype的,也就是指向Function.prototype,注意同时Object和Function也是对象所以下面诡异的代码也是成立的。

      console.log(Object instanceof Function);// true
      console.log(Function instanceof Object);// true
      console.log(Object.__proto__==Function.__proto__);// true
      console.log(Object.__proto__==Function.prototype);//true

根据上面的原型链知识接下来再来谈一谈js检测数据类型的四种方式和每种方式检测的粒度

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:

  • 基本类型:String、Number、Boolean、Symbol、Undefined、Null
  • 引用类型:Object

    基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。
    引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。
    鉴于 ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型。对于这个问题,JavaScript 也提供了多种方法,但遗憾的是,不同的方法得到的结果参差不齐。下面介绍常用的4种方法,并对各个方法存在的问题进行简单的分析。

1.typeof

    typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
    typeof '';// string 有效
    typeof 1;// number 有效
    typeof Symbol();// symbol 有效
    typeof true;//boolean 有效
    typeof undefined;//undefined 有效
    typeof null;//object 无效
    typeof [] ;//object 无效
    typeof new Function();// function 有效
    typeof new Date();//object 无效
    typeof new RegExp();//object 无效

有些时候,typeof 操作符会返回一些令人迷惑但技术上却正确的值:

  • 对于基本类型,除 null 以外,均可以返回正确的结果。
  • 对于引用类型,除 function 以外,一律返回 object 类型。
  • 对于 null ,返回 object 类型。
  • 对于 function 返回  function 类型。

其中,null 有属于自己的数据类型 Null , 引用类型中的 Array、Date、RegExp也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。

2、instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:

instanceof (A,B) = {
     
        const LP =Reflect.getPrototypeOf(A);
        const RP = B.prototype;
        if(LP === RP) {
     
            // A的内部属性 __proto__ 指向 B 的原型对象
            return true;
        }
        return false;
    }

从上述过程可以看出,当 A 的 [[Prototype]] 属性(__proto__) 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:

    [] instanceof Array;// true
    {
     } instanceof Object;// true
    new Date() instanceof Date;// true

    function Person(){
     };
    new Person() instanceof Person;//true

    [] instanceof Object;// true
    new Date() instanceof Object;// true
    new Person() instanceof Object;// true

我们发现,虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?

从 instanceof 能够判断出 [ ].__proto__  指向 Array.prototype,而 Array.prototype.__proto__ 又指向了Object.prototype,最终 Object.prototype.__proto__ 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
image

从原型链可以看出,[]__proto__ 直接指向Array.prototype,间接指向Object.prototype,所以按照instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    const XArray = window.frames[0].Array;
    const arr =new XArray(1,2,3);// [1,2,3]
    arr instanceof Array;// false

针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。

    if(Array.isArray(value)){
     
       //对数组执行某些操作
    }

Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。

3、constructor

当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。如下所示:
image
当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
image
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。同样,JavaScript 中的内置对象在内部构建时也是这样做的:
image
细节问题:

    1. null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
    1. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object。

image

为什么变成了 Object?因为 prototype 被重新赋值的是一个 { }, { } 是 new Object() 的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 { },也就是 Object 本身。因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。

4、toString

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString()  就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

    Object.prototype.toString.call('') ;  // [object String]
    Object.prototype.toString.call(1) ;   // [object Number]
    Object.prototype.toString.call(true) ;// [object Boolean]
    Object.prototype.toString.call(Symbol());//[object Symbol]
    Object.prototype.toString.call(undefined) ;// [object Undefined]
    Object.prototype.toString.call(null) ;// [object Null]
    Object.prototype.toString.call(newFunction()) ;// [object Function]
    Object.prototype.toString.call(newDate()) ;// [object Date]
    Object.prototype.toString.call([]) ;// [object Array]
    Object.prototype.toString.call(newRegExp()) ;// [object RegExp]
    Object.prototype.toString.call(newError()) ;// [object Error]
    Object.prototype.toString.call(document) ;// [object HTMLDocument]
    Object.prototype.toString.call(window) ;//[object global] window 是全局对象 global 的引用

Object.prototype.toString可以对变量的类型进行精确的检测,粒度最细建议大家在开发时采用这种方法检测变量类型。

下面是对Object.prototype.toString详细的阐述:

ECMAScript 3

在toString方法被调用时,会执行下面的操作步骤:

    获取this对象的[[Class]]属性的值.(第一步)

    计算出三个字符串"[object ", 第一步的操作结果Result(1), 以及 "]"连接后的新字符串.(第二步)

    返回第二步的操作结果Result(2). (第三步)

    [[Class]]是一个内部属性,所有的对象(原生对象和宿主对象)都拥有该属性.在规范中,[[Class]]是这么定义的
    [[Class]] 一个字符串值,表明了该对象的类型.
    所有内置对象的[[Class]]属性的值是由本规范定义的.
    所有宿主对象的[[Class]]属性的值可以是任意值,甚至可以是内置对象使用过的[[Class]]属性的值.
    [[Class]]属性的值可以用来判断一个原生对象属于哪种内置类型.需要注意的是,除了通过Object.prototype.toString方法之外,本规范没有提供任何其他方式来让程序访问该属性的值。
ECMAScript 5

在ECMAScript 5中,Object.prototype.toString()被调用时,会进行如下步骤:
1 如果 this是undefined ,返回 [object Undefined] ;
2 如果 this是null , 返回 [object Null] ;
3 令 O 为以 this 作为参数调用 ToObject 的结果;
4 令 class 为 O 的内部属性 [[Class]] 的值;
5 返回三个字符串 “[object”, class, 以及"]" 拼接而成的字符串。

[[Class]]是一个内部属性,值为一个类型字符串,可以用来判断值的类型。
有这么一段详细的解释:

     本规范的每种内置对象都定义了 [[Class]] 内部属性的值。宿主对象的 [[Class]] 内部属性的值可以是除了 "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String" 的任何字符串。[[Class]] 内部属性的值用于内部区分对象的种类。注,本规范中除了通过 Object.prototype.toString ( 见 15.2.4.2) 没有提供任何手段使程序访问此值。
    在JavaScript代码里,唯一可以访问该属性的方法就是通过Object.prototype.toString 。

通常方法如下:

    Object.prototype.toString.call(value);

可以用下列函数,来获取任意变量的[[Class]]属性:

    function getClass (a) {
      
        const str = Object.prototype.toString.call(a);
        return /^\[object (.*)\]$/.exec(str)[1];
   }

js的内置类型和一起其他常用对象都重写了toString方法,实例如下:
js原型链和变量类型检测的方式和粒度_第2张图片

ECMAScript 6
1 在ES6,调用 Object.prototype.toString 时,会进行如下步骤:
2 如果 this 是 undefined ,返回 ‘[object Undefined]’ ; (函数直接返回分支)
3 如果 this 是 null , 返回 ‘[object Null]’ ;(函数直接返回分支)
4 令 O 为以 this 作为参数调用 ToObject 的结果;(调用ToObject函数获取结果O)
5 令 isArray 为 IsArray(O) ;(调用IsArray(O) 函数获取结果赋值给isArray )
6 ReturnIfAbrupt(isArray) (如果 isArray 不是一个正常值,比如抛出一个错误,中断执行);
7 如果 isArray 为 true , 令 builtinTag 为 ‘Array’ ;
8 else ,如果 O is an exotic String object , 令 builtinTag 为 ‘String’ ;
9 else ,如果 O 含有 [[ParameterMap]] internal slot, , 令 builtinTag 为 ‘Arguments’ ;
10 else ,如果 O 含有 [[Call]] internal method , 令 builtinTag 为 Function ;
11 else ,如果 O 含有 [[ErrorData]] internal slot , 令 builtinTag 为 Error ;
12 else ,如果 O 含有 [[BooleanData]] internal slot , 令 builtinTag 为 Boolean ;
13 else ,如果 O 含有 [[NumberData]] internal slot , 令 builtinTag 为 Number ;
14 else ,如果 O 含有 [[DateValue]] internal slot , 令 builtinTag 为 Date ;
15 else ,如果 O 含有 [[RegExpMatcher]] internal slot , 令 builtinTag 为 RegExp ;
16 else , 令 builtinTag 为 Object ;
17 令 tag 为 Get(O, @@toStringTag) 的返回值( Get(O, @@toStringTag) 方法,既是在 O 是一个对象,并且具有 @@toStringTag 属性时,返回 O[Symbol.toStringTag] );
18 ReturnIfAbrupt(tag) ,如果 tag 是正常值,继续执行下一步;
如果 Type(tag) 不是一个字符串,let tag be builtinTag ;
19 返回由三个字符串 “[object”, tag, “]” 拼接而成的一个字符串。

注意

    1. 步骤1,2是Object.prototype.toString(null)和> Object.prototype.toString(undefined )的特殊入参的处理
    1. 步骤7到16是获取builtinTag变量的值;
    1. 步骤17的解释:
       let obj = {
     };
       Object.defineProperty(obj, Symbol.toStringTag, {
     
            get: function() {
     
                return "newClass"
            }
       });

关于对象的Symbol.toStringTag属性的作用请移步MDN官方文档Symbol.toStringTag属性的作用

ECMAScript 7
    ES7目前还是工作草案,到目前为止,就 Object.prototype.toString 的实现步骤来说, 只是移除了ES6其中的 ReturnIfAbrupt 
附言

Object对象和它的原型链上各自有一个toString()方法,第一个返回的是一个函数,第二个返回的是值类型。

    Object.toString.call(Array)//"function Array() { [native code] }"
    Object.prototype.toString.call(Array)//"[object Function]"
    Object.toString()//"function Object() { [native code] }"
    Object.prototype.toString()//"[object Object]"

另外再给大家推荐一个变量检测高阶函数函数的写法:

    
/**
 * @todo 生成完善的类型检测函数
 * @param {String} type 需要生成检测函数的类型 
 *        可选值 string|number|null|undefined|object|array|function|symbol|date|regexp|boolean
 * @returns {undefined|Function}
 */

function isType(type){
     
    type=type.trim();
    //强制首字母大写
    if(type.length>0){
     
        type=type.toLowerCase();
        //正则表达式需要两个字母大写 RegExp 比较特殊
        if(type==='regexp'){
     
            type='RegExp';
        }else{
     
            type=type.split('').map((c,index)=>index==0?c.toUpperCase():c).join('');
        }
    }else{
     
        return;
    }
    const cache=arguments.callee.cache?arguments.callee.cache:(arguments.callee.cache={
     });
    //走缓存
    if(cache[type]){
     
        return cache[type];
    }
    //用函数表达式 而不是函数声明 防止变量提升
    const handler=function (obj){
     
        return Object.prototype.toString.call(obj)===`[object ${
       type}]`;
    }
    //第一次进行缓存
    cache[type]=handler;
    return handler;
}

const isString=isType('string');
const isArray=isType('Array');
const isDate=isType('date');
const isReg=isType('RegExp');
const isNull=isType('null');
const isFunction=isType('function');
const isUndefined=isType('undefined');
const isObject=isType('object');
const isSymbol=isType('symbol');
const isNumber=isType('number');
const isBoolean=isType('boolean');
const isPromise=isType('promise');



isString('STR')//true
isString(123) //false

你可能感兴趣的:(javascript)