资讯专栏INFORMATION COLUMN

JavaScript 类型的那些事

lwx12525 / 2666人阅读

摘要:类型判断类型检测主要包括了和的三种方式来判断变量的类型。对于这里的返回的确却是,,有些人说被认为是没有一个对象。但是各种运算符或条件判断中是需要特定类型的,比如判断时会将判断语句转换为布尔型。顾名思义就是将变量转换为对象类型。

概述

JavaScript的类型判断是前端工程师们每天代码中必备的部分,每天肯定会写上个很多遍if (a === "xxx")if (typeof a === "object")类似的类型判断语句,所以掌握JavaScript中类型判断也是前端必备技能,以下会从JavaScript的类型,类型判断以及一些内部实现来让你深入了解JavaScript类型的那些事。

类型

JavaScript中类型主要包括了primitiveobject类型,其中primitive类型包括了:nullundefinedbooleannumberstringsymbol(es6)。其他所有的都为object类型。

类型判断

类型检测主要包括了:typeofinstanceoftoString的三种方式来判断变量的类型。

typeof

typeof接受一个值并返回它的类型,它有两种可能的语法:

typeof x

typeof(x)

当在primitive类型上使用typeof检测变量类型时,我们总能得到我们想要的结果,比如:

typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"

而当在object类型上使用typeof检测时,有时可能并不能得到你想要的结果,比如:

typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"

这里的[]返回的确却是object,这可能并不是你想要的,因为数组是一个特殊的对象,有时候这可能并不是你想要的结果。

对于这里的null返回的确却是object,wtf,有些人说null被认为是没有一个对象。

当你对于typeof检测数据类型不确定时,请谨慎使用。

toString

typeof的问题主要在于不能告诉你过多的对象信息,除了函数之外:

typeof {key:"val"}; // Object is object
typeof [1,2]; // Array is object
typeof new Date; // Date object

toString不管是对于object类型还是primitive类型,都能得到你想要的结果:

var toClass = {}.toString;

console.log(toClass.call(123));
console.log(toClass.call(true));
console.log(toClass.call(Symbol("foo")));
console.log(toClass.call("some string"));
console.log(toClass.call([1, 2]));
console.log(toClass.call(new Date()));
console.log(toClass.call({
    a: "a"
}));

// output
[object Number]
[object Boolean]
[object Symbol]
[object String]
[object Array]
[object Date]
[object Object]

underscore中你会看到以下代码:

// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
  each(["Arguments", "Function", "String", "Number", "Date", "RegExp"], function(name) {
    _["is" + name] = function(obj) {
      return toString.call(obj) == "[object " + name + "]";
    };
  });

这里就是使用toString来判断变量类型,比如你可以通过_.isFunction(someFunc)来判断someFunc是否为一个函数。

从上面的代码我们可以看到toString是可依赖的,不管是object类型还是primitive类型,它都能告诉我们正确的结果。但它只可以用于判断内置的数据类型,对于我们自己构造的对象,它还是不能给出我们想要的结果,比如下面的代码:

function Person() {
}

var a = new Person();
// [object Object]
console.log({}.toString.call(a));
console.log(a instanceof Person);

我们这时候就要用到我们下面介绍的instanceof了。

instanceof

对于使用构造函数创建的对象,我们通常使用instanceof来判断某一实例是否属于某种类型,例如:a instanceof Person,其内部原理实际上是判断Person.prototype是否在a实例的原型链中,其原理可以用下面的函数来表达:

function instance_of(V, F) {
  var O = F.prototype;
  V = V.__proto__;
  while (true) {
    if (V === null)
      return false;
    if (O === V)
      return true;
    V = V.__proto__;
  }
}

// use
function Person() {
}
var a = new Person();

// true
console.log(instance_of(a, Person));
类型转换

因为JavaScript是动态类型,变量是没有类型的,可以随时赋予任意值。但是各种运算符或条件判断中是需要特定类型的,比如if判断时会将判断语句转换为布尔型。下面就来深入了解下JavaScript中类型转换。

ToPrimitive

当我们需要将变量转换为原始类型时,就需要用到ToPrimitive,下面的代码说明了ToPrimitive的内部实现原理:

// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
  // Fast case check.
  if (IS_STRING(x)) return x;
  // Normal behavior.
  if (!IS_SPEC_OBJECT(x)) return x;
  if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive);
  if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
  return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
  if (!IS_SYMBOL_WRAPPER(x)) {
    var valueOf = x.valueOf;
    if (IS_SPEC_FUNCTION(valueOf)) {
      var v = %_CallFunction(x, valueOf);
      if (IsPrimitive(v)) return v;
    }

    var toString = x.toString;
    if (IS_SPEC_FUNCTION(toString)) {
      var s = %_CallFunction(x, toString);
      if (IsPrimitive(s)) return s;
    }
  }
  throw MakeTypeError(kCannotConvertToPrimitive);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
  if (!IS_SYMBOL_WRAPPER(x)) {
    var toString = x.toString;
    if (IS_SPEC_FUNCTION(toString)) {
      var s = %_CallFunction(x, toString);
      if (IsPrimitive(s)) return s;
    }

    var valueOf = x.valueOf;
    if (IS_SPEC_FUNCTION(valueOf)) {
      var v = %_CallFunction(x, valueOf);
      if (IsPrimitive(v)) return v;
    }
  }
  throw MakeTypeError(kCannotConvertToPrimitive);
}

上面代码的逻辑是这样的:

如果变量为字符串,直接返回

如果!IS_SPEC_OBJECT(x),直接返回

如果IS_SYMBOL_WRAPPER(x),则抛出异常

否则会根据传入的hint来调用DefaultNumberDefaultString,比如如果为Date对象,会调用DefaultString

DefaultNumber:首先x.valueOf,如果为primitive,则返回valueOf后的值,否则继续调用x.toString,如果为primitive,则返回toString后的值,否则抛出异常

DefaultString:和DefaultNumber正好相反,先调用toString,如果不是primitive再调用valueOf

那讲了实现原理,这个ToPrimitive有什么用呢?实际很多操作会调用ToPrimitive,比如相等比较操。在进行操作时会将左右操作数转换为primitive,然后进行相加。

下面来个实例,({}) + 1(将{}放在括号中是为了内核将其认为一个代码块)会输出啥?可能日常写代码并不会这样写,不过网上出过类似的面试题。

操作只有左右运算符同时为StringNumber时会执行对应的%_StringAdd%NumberAdd,下面看下({}) + 1内部会经过哪些步骤:

{}1首先会调用ToPrimitive

{}会走到DefaultNumber,首先会调用valueOf,返回的是Object {},不是primitive类型,从而继续走到toString,返回[object Object],是String类型

最后加操作,结果为[object Object]1

再比如有人问你[] + 1输出啥时,你可能知道应该怎么去计算了,先对[]调用ToPrimitive,返回空字符串,最后结果为"1"

除了ToPrimitive之外,还有更细粒度的ToBooleanToNumberToString,比如在需要布尔型时,会通过ToBoolean来进行转换。看一下源码我们可以很清楚的知道这些布尔型、数字等之间转换是怎么发生:

// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
  if (IS_BOOLEAN(x)) return x;
  // 字符串转布尔型时,如果length不为0就返回true
  if (IS_STRING(x)) return x.length != 0;
  if (x == null) return false;
  // 数字转布尔型时,变量不为0或NAN时返回true
  if (IS_NUMBER(x)) return !((x == 0) || NUMBER_IS_NAN(x));
  return true;
}

// ECMA-262, section 9.3, page 31.
function ToNumber(x) {
  if (IS_NUMBER(x)) return x;
  // 字符串转数字调用StringToNumber
  if (IS_STRING(x)) {
    return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
                                    : %StringToNumber(x);
  }
  // 布尔型转数字时true返回1,false返回0
  if (IS_BOOLEAN(x)) return x ? 1 : 0;
  // undefined返回NAN
  if (IS_UNDEFINED(x)) return NAN;
  // Symbol抛出异常,例如:Symbol() + 1
  if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToNumber);
  return (IS_NULL(x)) ? 0 : ToNumber(DefaultNumber(x));
}

// ECMA-262, section 9.8, page 35.
function ToString(x) {
  if (IS_STRING(x)) return x;
  // 数字转字符串,调用内部的_NumberToString
  if (IS_NUMBER(x)) return %_NumberToString(x);
  // 布尔型转字符串,true返回字符串true
  if (IS_BOOLEAN(x)) return x ? "true" : "false";
  // undefined转字符串,返回undefined
  if (IS_UNDEFINED(x)) return "undefined";
  // Symbol抛出异常
  if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToString);
  return (IS_NULL(x)) ? "null" : ToString(DefaultString(x));
}

讲了这么多原理,那这个ToPrimitive有什么卵用呢?这对于我们了解JavaScript内部的隐式转换和一些细节是非常有用的,比如:

var a = "[object Object]";
if (a == {}) {
    console.log("something");
}

你觉得会不会输出something呢,答案是会的,所以这也是为什么很多代码规范推荐使用===三等了。那这里为什么会相等呢,是因为进行相等操作时,对{}调用了ToPrimitive,返回的结果就是[object Object],也就返回了true了。我们可以看下JavaScript中EQUALS的源码就一目了然了:

// ECMA-262 Section 11.9.3.
EQUALS = function EQUALS(y) {
  if (IS_STRING(this) && IS_STRING(y)) return %StringEquals(this, y);
  var x = this;

  while (true) {
    if (IS_NUMBER(x)) {
      while (true) {
        if (IS_NUMBER(y)) return %NumberEquals(x, y);
        if (IS_NULL_OR_UNDEFINED(y)) return 1;  // not equal
        if (IS_SYMBOL(y)) return 1;  // not equal
        if (!IS_SPEC_OBJECT(y)) {
          // String or boolean.
          return %NumberEquals(x, %$toNumber(y));
        }
        y = %$toPrimitive(y, NO_HINT);
      }
    } else if (IS_STRING(x)) {
      // 上面的代码就是进入了这里,对y调用了toPrimitive
      while (true) {
        if (IS_STRING(y)) return %StringEquals(x, y);
        if (IS_SYMBOL(y)) return 1;  // not equal
        if (IS_NUMBER(y)) return %NumberEquals(%$toNumber(x), y);
        if (IS_BOOLEAN(y)) return %NumberEquals(%$toNumber(x), %$toNumber(y));
        if (IS_NULL_OR_UNDEFINED(y)) return 1;  // not equal
        y = %$toPrimitive(y, NO_HINT);
      }
    } else if (IS_SYMBOL(x)) {
      if (IS_SYMBOL(y)) return %_ObjectEquals(x, y) ? 0 : 1;
      return 1; // not equal
    } else if (IS_BOOLEAN(x)) {
      if (IS_BOOLEAN(y)) return %_ObjectEquals(x, y) ? 0 : 1;
      if (IS_NULL_OR_UNDEFINED(y)) return 1;
      if (IS_NUMBER(y)) return %NumberEquals(%$toNumber(x), y);
      if (IS_STRING(y)) return %NumberEquals(%$toNumber(x), %$toNumber(y));
      if (IS_SYMBOL(y)) return 1;  // not equal
      // y is object.
      x = %$toNumber(x);
      y = %$toPrimitive(y, NO_HINT);
    } else if (IS_NULL_OR_UNDEFINED(x)) {
      return IS_NULL_OR_UNDEFINED(y) ? 0 : 1;
    } else {
      // x is an object.
      if (IS_SPEC_OBJECT(y)) {
        return %_ObjectEquals(x, y) ? 0 : 1;
      }
      if (IS_NULL_OR_UNDEFINED(y)) return 1;  // not equal
      if (IS_SYMBOL(y)) return 1;  // not equal
      if (IS_BOOLEAN(y)) y = %$toNumber(y);
      x = %$toPrimitive(x, NO_HINT);
    }
  }
}

所以了解变量如何转换为primitive类型的重要性也就可想而知了。具体的代码细节可以看这里:runtime.js。

ToObject

ToObject顾名思义就是将变量转换为对象类型。可以看下它是如何将非对象类型转换为对象类型:

// ECMA-262, section 9.9, page 36.
function ToObject(x) {
  if (IS_STRING(x)) return new GlobalString(x);
  if (IS_NUMBER(x)) return new GlobalNumber(x);
  if (IS_BOOLEAN(x)) return new GlobalBoolean(x);
  if (IS_SYMBOL(x)) return %NewSymbolWrapper(x);
  if (IS_NULL_OR_UNDEFINED(x) && !IS_UNDETECTABLE(x)) {
    throw MakeTypeError(kUndefinedOrNullToObject);
  }
  return x;
}

因为日常代码很少用到,就不展开了。

本文首发于有赞技术博客:http://tech.youzan.com/javasc...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84323.html

相关文章

  • JS异步那些 一 (基础知识)

    摘要:异步那些事一基础知识异步那些事二分布式事件异步那些事三异步那些事四异步那些事五异步脚本加载事件概念异步回调首先了讲讲中两个方法和定义和用法方法用于在指定的毫秒数后调用函数或计算表达式。功能在事件循环的下一次循环中调用回调函数。 JS异步那些事 一 (基础知识)JS异步那些事 二 (分布式事件)JS异步那些事 三 (Promise)JS异步那些事 四(HTML 5 Web Workers...

    李涛 评论0 收藏0
  • Javascript 对象那些(持续更新)

    摘要:一前言记录语言类型的一些问题。其它浏览器则完全按照对象定义的顺序遍历属性。所以,顺序这种事,还是要用数组来保证。详细请参考对象遍历顺序三后记参考链接对象遍历顺序 一 前言 记录javascript语言object类型的一些问题。 1. typeof []; // object 2. typeof {};// object 3. typeof null; //objec...

    CoreDump 评论0 收藏0
  • JavaScript 继承那些

    摘要:实际上也就是在原型链继承的代码中添加在子类的构造函数中调用父类构造函数。寄生组合式继承在指定子类的原型的时候不必调用父类的构造函数,而是直接使用创建父类原型的副本。 原本地址:http://www.ahonn.me/2017/01/2... 众所周知,JavaScript 的继承是实现继承,而没有 Java 中的接口继承。这是因为 JavaScript 中函数没有签名,而实现继承依靠的...

    singerye 评论0 收藏0
  • JS异步那些 五 (异步脚本加载)

    摘要:遵循的是异步模块定义规范,遵循的是通用模块定义规范。不同的脚本加载这个模块,得到的都是同一个实例。关于异步那些事就写到这里了,很多地方理解的不够深刻希望大家多多指教。 JS异步那些事 一 (基础知识)JS异步那些事 二 (分布式事件)JS异步那些事 三 (Promise)JS异步那些事 四(HTML 5 Web Workers)JS异步那些事 五 (异步脚本加载) 异步脚本加载 阻塞性...

    terasum 评论0 收藏0
  • 关于Cookie那些

    摘要:假设有两个域名域名域名域名有分级的概念,也就是说域名与域名都是的子域名,又是的子域名在域名所使用的服务中,可以设置域名在服务端设置的时候,设置为或没有区别,注意前面的点,即只要是为显式的声明,前面带不带点没有区别。 1 Cookie简介 Cookie是由W3C组织提出,最早由NetScape社区发展的一种机制。Cookie是存储于访问者的计算机中的变量。每当同一台计算机通过浏览器请求某...

    sf_wangchong 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<