js类型----你所不知道的JavaScript系列(5)

ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型。也有其他的叫法,比如原始类型和对象类型等。

1、内置类型

JavaScript 有七种内置类型:
 • 空值(null
 • 未定义(undefined
 • 布尔值(boolean
 • 数字(number
 • 字符串(string
 • 对象(object
 • 符号(symbolES6 中新增)

除对象之外,其他统称为“基本类型”。 对象称为“引用类型”。

我们可以用 typeof 运算符来查看值的类型,它返回的是类型的字符串值。有意思的是,这七种类型和它们的字符串值并不一一对应: 

typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// ES6中新加入的类型
typeof Symbol() === "symbol"; // true

以上六种类型均有同名的字符串值与之对应。 你可能注意到 null 类型不在此列。它比较特殊typeof 对它的处理有问题:

typeof null === "object"; // true

按照类型的定义,这里正确的返回结果应该是 "null"。但这个 bug 由来已久,在 JavaScript 中已经存在了将近二十年,也许永远也不会修复,因为这牵涉到太多的 Web 系统,“修复”它会产生更多的bug,令许多系统无法正常工作。 

所以在日常开发中,为了更好的检测null类型,我们需要使用复合条件来检测 null 值的类型: 

var a = null;
(!a && typeof a === "object"); // true

null 是基本类型中唯一的一个“假值”类型, typeof对它的返回值为 "object"

这里还有一张情况,看下面:

typeof function a(){ /* .. */ } === "function"; // true

或许这里你会说js的内置类型没有function类型呀,为什么这里typeof会返回"function"。在介绍里面,function类型实际上是 object 的一个“子类型”。具体来说,函数是“可调用对象”,它有一个内部属性 [[Call]],该属性使其可以被调用。 

函数不仅是对象,还可以拥有属性。例如:

function a(b,c) {
   /* .. */
}

a.length; // 2  函数对象的 length 属性是其声明的参数的个数

a.name;  // "a" 函数对象的 name 属性返回函数名

再来看看数组。 JavaScript 支持数组 ,数组也是对象,它也是 object 的一个“子类型”,数组的元素按数字顺序来进行索引(而非普通像对象那样通过字符串键值),其 length 属性是元素的个数。

typeof [1,2,3] === "object"; // true

我们来比较一下下面的代码:

typeof   null;   // "object"
typeof   [];   // "object"
typeof   {};   // "object"

返回的都是"object",那这样我们要怎么知道区分对象,数组和null呢?

其实方法有很多,这里罗列几种给大家。

 

1、instanceof

instanceof 是用来判断 A 是否为 B 的实例对,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。 在这里需要特别注意的是:instanceof检测的是原型。其内部原理大致可以这样理解:

instanceof (A,B) = {
    var L = A.__proto__;
    var R = B.prototype;
    if(L === R) {
        //A的内部属性__proto__指向B的原型对象
        return true;
    }
    return false;
}

虽然上述代码很不严谨也有一些问题,但是从上述过程可以看出,当 A 的 __proto__ 指向 B 的 prototype 时,就认为A就是B的实例,我们再来看几个例子:

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

细心的你可能发现了 [] instanceof Array 和 [] instanceof Object 竟然都返回 true。为什么?回想一下前面我讲过的“instanceof检测的是原型”,在刚才的例子里面就涉及到了原型和原型链的概念。如果对原型和原型链不了解的同学,可以先去看看相关知识,这个知识点是十分重要的,不管是以后工作还是面试,都是非常重要的!那么。。。现在继续赚回来看我们的例子。从原型链上来说 [].__proto__ 指向 Array.prototype, 而 Array.prototype.__proto__ 又指向了Object.prototype,Object.prototype.__proto__ 指向了null,标志着原型链的结束。所以按照 instanceof 的判断规则,[] 就是Object的实例。当然,类似的new Date()、new Person() 也会形成这样一条原型链,例如:new Date() instanceof Object 也会返回 true。因此,instanceof 只能用来判断两个对象是否属于原型链的关系, 而不能获取对象的具体类型。

 

2、Object.prototype.toString.call

最强利器!!

Object.prototype.toString.call('') ;   // "[object String]"
Object.prototype.toString.call(1) ;    // "[object Number]"
Object.prototype.toString.call(true) ; // "[object Boolean]"
Object.prototype.toString.call(undefined) ; // "[object Undefined]"
Object.prototype.toString.call(null) ; // "[object Null]"
Object.prototype.toString.call(new Function()) ; // "[object Function]"
Object.prototype.toString.call(new Date()) ; // [object Date]"
Object.prototype.toString.call([]) ; // "[object Array]"
Object.prototype.toString.call({}) ; //"[object Object]"

基本上所有对象的类型都可以通过这个方法获取到。也可以简写成 toString.call ,例如 toString.call({ }),toString.call([ ])

 

 

2、值和类型

 JavaScript 中的变量是没有类型的, 只有值才有。变量可以随时持有任何类型的值。   ----《你所不知道的JavaScript(中)》P6

JavaScript 不做“类型强制”,也就是说,JavaScript 拥有动态类型,这意味着相同的变量可用作不同的类型。语言引擎不要求变量总是持有与其初始值同类型的值。一个变量可以现在被赋值为字符串类型值,随后又被赋值为数字类型值。

42 的类型为 number,并且无法更改。而 "42" 的类型为 string。数字 42 可以通过强制类型转换(coercion)为字符串 "42" 。

在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型,因为 JavaScript 中的变量没有类型

var a = 42;
typeof a; // "number"

a = true; typeof a; // "boolean"

 

 

3、值和引用

开头曾说过,ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型。为什么有这两种区分呢?我们先来看下面的例子:

var a = 2;
var b = a; 
b++;
a; // 2
b; // 3

var c = [1,2,3]; var d = c; d.push( 4 ); c; // [1,2,3,4] d; // [1,2,3,4]

在JavaScript中,简单值(即标量基本类型值, scalar primitive总是通过值复制的方式来赋值 / 传递,包括nullundefined、字符串、数字、布尔和 ES6 中的 symbol。 复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则
通过引用复制的方式来赋值 / 传递。
上例中 2 是一个标量基本类型值,所以变量 a 持有该值的一个复本, b 持有它的另一个复
本。 b 更改时, a 的值保持不变。c d 则分别指向同一个复合值 [1,2,3] 的两个不同引用。请注意, c d 仅仅是指向值[1,2,3],并非持有。所以它们更改的是同一个值(如调用 .push(4))。随后它们都指向更改后的新值 [1,2,3,4]。 

 

由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。 

var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// 然后
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]

代码前两句使得a、b都是对复合值 [1,2,3] 的两个不同引用,所以代码第三第四句输出的结果就都是值[1,2,3]。代码第五句b=[4,5,6] 的执行使得变量b重新指向了另一个复合值,而不会修改的原先的复合值,也不会不影响 a 指向值 [1,2,3],所以a输出的值依旧是复合值[1,2,3]。 

有了上面的例子之后,再来看看下面一段代码,

function foo(x) {
   x.push( 4 );
   x; // [1,2,3,4]
   // 然后
   x = [4,5,6];
   x.push( 7 );
   x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[1,2,3,4],不是[4,5,6,7]

我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]在函数中我们可以通过引用 x 来更改数组的值(push(4) 之后变为 [1,2,3,4])。但 x =[4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]。 

我们不能通过引用 x 来更改引用 a 的指向,只能更改 a x 共同指向的值 。如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

function foo(x) {
   x.push( 4 );
   x; // [1,2,3,4]
   // 然后
   x.length = 0; // 清空数组
   x.push( 4, 5, 6, 7 );
   x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]

x.length = 0 x.push(4,5,6,7) 并没有创建一个新的数组,而是更改了当前的数组。于是 a 指向的值变成了 [4,5,6,7]。 

关于引用类型的比较和基本类型的比较,我们来看下面的代码:

//基础类型的比较
var a = 1;
var b = 1;

a===b;   // true


//引用类型比较
var c = [1];
var d = c;
var e = [1];

c===d;   // true
c===e;   // false

基础类型的比较是值的比较,只要值相等,就返回 true,而引用类型的比较是两个引用的比较,上面例子中c、d都是对复合值 [1] 的应用,他们都指向同一个值,所以返回 true ,虽然 e 所指的值也是 [1],但是这个 [1] 和 c 所指的 [1] 不一定,c和e是两个不同的引用,就像C++里面所说的指针,虽然两个指针所指的内存区域存放的值一样,但是这两个指针所指的地址不一样,在机器看来这就是两个完全不同的变量,所以在比较的时候就返回 false 。

 

总之请谨记:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。

 

 



posted @ 2018-07-18 16:23  李某龙  阅读(1654)  评论(0编辑  收藏  举报