类型和原生函数及类型转换(一)

一、内置类型:

空值:null

未定义:undefined

布尔值:boolean

数字:number

字符串:string

对象:object

符号:symbol(ES6新增)

1.null类型的值类型是object,因为JavaScript语言这一历史遗留问题,如果要查看null的类型不能直接使用typeof,需要设置复合条件来查询:

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

2.function作为object的子类型,用typeof查看其类型时显示的是function。

function a (){
    //888
}
console.log(typeof a);//function

function除了类型这一特性比较特殊,还有length属性表示的是函数声明的参数的个数。

function b(a,b){
    /*..*/
}
console.log(b.length);//2

3.数组Array类型也是object的一个子类型,但是在使用typeof查看类型时却是object,这点与function有些差别,需要注意。

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

4.JavaScript的变量是没有类型,其类型是由值来决定的,变量可以随时持有任何类型的值。

var a = 123;
typeof a;//number
a = true;
typeof a;//boolean
//顺便在这里说明一下typeof运算符返回的值始终是一个字符串
typeof typeof 88;//"string"

5.undefined和undeClared

可能很多人都认为undefined和undeClared是同一种含义,但是在JavaScript中他们完全是两回事。undefined表示声明未赋值,undeClared表示没有声明。但是使用typeof检测类型时都是返回undefined,这就是非常的尴尬了。

console.log(typeof a);    //undefined -- typeof只执行类型检测不会隐式添加全局变量
if(a){                    //这里会报错
    console.log("aa");
}

其实typeof对undeClared(未声明的变量)检测返回undefined是一种安全机制,我们都知道JavaScript语言最开始的设计目的就是为了尽可能的保证程序可执行,因为多脚本异步加载且共享全局命名空间,如果我们需要检测一个全局变量是否声明,或者兼容API:

if(b){    //报错
    //..
}
if(typeof b!== "undefined"){
    //..这里才会真正保证不出错
}
if(typeof abc === "undefined"){
    abc = function(){...}//兼容API
}
//在全局上检测变量是否声明还有一个办法
if(window.b){
    //...
}
if(!window.b){
    //...
}

关于undefined还有一些其他特性,与这期博客的内容没有什么相关性,在我的另一篇博客有详细介绍:JavaScript中调皮的undefined

 二、数组与字符串的值

字符串在很多时候被误解为字符组成的数组,因为都有length属性和ES5支持concat(...)拼接方法,还有些时候在一些内置方法里呈现出相识的特性,这绝对不能说,字符串是字符数组。

var a = "foo";
var b = ["f","o","o"];
console.log(a.length);//3
console.log(b.length);//3
console.log(a.indexOf("o"));//1
console.log(b.indexOf("o"));//1
var c = a.concat("bar");        //foobar
var d = b.concat(["b","a","r"]);//["f","o","o","b","a","r"]
console.log(a === c);//false
console.log(b === d);//false
console.log(a);//"foo"
console.log(b);//["f","o","o"]
console.log(a[1]);//"o"
console.log(b[1]);//"o"

上面这些代码好像可以说字符串和数组没有区别,但是在这里下结论还为时过早,值得留意的是IE中a[1]这种写法是不符合语法的。正确是做法是a.charAt(1)。最大的区别就是字符串的成员函数不会改变其原始值,而数组的成员函数都是在其原始值上操作。

var a = "foo";
var b = ["f","o","o"];
var c = a.toUpperCase();
console.log(a === c);//false
b.push("!");
console.log(b);//["f","o","o","!"]

因为字符串与数组存在很多相识的特性,所以很多字符串操作借用数组的成员方法来实现就会方便很多。

var a = "foo";
var c = Array.prototype.join.call(a,"-");
var d = Array.prototype.map.call(a,function(v){
    return v.toUpperCase() + ".";
});
console.log(c);//"f-o-o"
console.log(d);//["F.", "O.", "O."]
var e = Array.prototype.map.call(a,function(v){
    return v.toUpperCase() + ".";
}).join("");
console.log(e);//"F.O.O."

关于字符串借用数组的成员方法实现操作有一个很经典的面试题,就是实现字符串的反转操作,我们知道在字符串的成员方法中没有像数组一样的reverse()。但是我们可以通过字符串与数组相似的特性,借用数组的成员方法来实现。但绝对不是Array.prototype.reverse.call(...)这么简单的操作。

var a = "foo";
console.log(Array.prototype.reverse.call(a));//报错
var c = a.split("").reverse().join("");
console.log(c);//"oof"

关于字符串与数组的值在一定程度上存在很多类似特性,并不是值本身具备的特性,这里涉及到了字符串操作时的隐式类型转换操作,在这里暂时不做介绍,后面会有详细的博客来介绍关于类型转换的知识点。而关于数组的值本身还有一个非常令人费解的问题,这个问题就是“稀疏”数组,这个问题有时候会让你抓狂。而这一部分需要在原生函数的基础上来解析,所以我们先来了解原生函数。

三、原生函数

 在前面介绍变量类型(严格来说是值类型)的时候,我没有做分类这些基本的解析,不是忘记,而是需要更多的知识点来佐证一些内容,涉及JavaScript的值类型和内置的typeof的值类型判断,还有toString()的机制,甚至一些操作带来的隐式类型转换不只是在变量类型的基础上能说的清楚的。这里面有很大一部分的内容就需要原生函数来回答。

1.值类型分类:

  原始值:number、boolean、string、undefined、null

  引用值:array、object、function、tate、RegExp

2.一个被忽略的问题,原始值类型有属性和方法?

在JavaScript中一直有一个误导的说法,就是万物皆对象。但是我要告诉你,原始值没有属性和方法,JavaScript的原始值本身也不是对象,而原始值在有时候能呈现对象的特性是因为在操作的背后隐式的存在类型转换,这个隐式的转换就是显式的原生函数构造(封装)基本类型值的封装对象。所以不要误认为JS中的构造函数和JAVA中的构造函数是一样的,在JS中构造函数封装获得的是一个对象,并不是JS中的原始值类型。

var a = "abc";
var b = new String("abc");
console.log(a === b);//false
console.log(a == b);//true
console.log(typeof a);//string
console.log(typeof b);//object

3.聊了这么多原生函数,到底什么是原生函数?原生函数怎么使用?原生函数有什么用呢?

通过原生函数(如new String("abc"))创建的是一个封装了基本类型值的封装对象,那JavaScript中都有哪些原生函数呢?String()、Number()、Boolean()、Array()、Object()、Function()、RegExp()、Date()、Error()、Symbol()。

通过原生函数构造基本类型值的封装对象:

//值类型
var a = "abc";
//通常的基础类型对象封装方法(String类型对象)
var b = new String("abc");
//通过基础类型值变量直接封装
var c = Object(a);
console.log(c);//String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
console.log(b);//String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

封装基础类型对象的语法很简单,这里就不累述了,细心的你应该发现原生函数和值类型并不一一对应,undefined和null没有对应的原生函数。另一问题的答案也浮出了水面,那就是原始值类型没有属性和方法,那原始值具备的对象特性从何而来,有一部分已经可以从上面的示例代码中找到答案了,我们可以看到String对象有length属性,前面提到的字符串类似数组的一些特性,都可以在上面的代码中得到答案,就是这些操作的背后都存在着隐式的基础类型封装操作。当我们用字符串调用length属性时,浏览器引擎帮我们隐式的封装了一个String对象,这个length属性就是由这个隐式封装的对象提供的。(关于类型转换和隐式类型转换会有更详细的博客文章解析,这篇博客不过多介绍。)

4.引用值类型的原生函数及其构造的对象

 因为引用值类型本身就是object类型或者object的子类型,通过原生函数构造的对象与常量形式创建的对象没有区别,但是反而通过原生函数构建的对象在有时候会产生意想不到的bug。(珍惜生命,远离构造函数。)

a.数组的元素函数Array(...):

var a = new Array(1,2,3);
console.log(a);//[1,2,3]
var b = [1,2,3];
console.log(b);//[1,2,3]

根据示例来看好像没啥区别,但是当我们只给原生函数添加一个参数时,这个数字参数定义的却是数组的长度。这和我们前面提到的稀松数组出现的问题是一样的,浏览器对这种情况表现的就大相庭径,以下方面的示例来说:

var a = new Array(3);
console.log(a.length);//3

然而我们通过各个浏览器的控制台打印数组时,给我们的结果却令人费解。(因为我使用的都是最新版的浏览器,所以结果和你的可能会有些出入。)在老版本的Chrome中打印的是[undefined,undefined,undefined],老版本的Firefox打印的是[,,,]。

IE打印的是:[]

Chrome打印的是:(3) [empty × 3]

Firefox打印的是:[ <3 empty slots> ]

var a = new Array(3);
console.log(a[1]);//undefine

浏览器的表现绝不是空悬来风,看下面的代码你会更加头痛:

var a = new Array(3);
var b = [undefined,undefined,undefined];
console.log(a[1]);//undefined
console.log(b[1]);//undefined
console.log(a.map(Math.sqrt));//(3) [empty × 3]
console.log(b.map(Math.sqrt));//(3) [NaN, NaN, NaN]

//模仿join方法 function fakeJoin(arr,connector){ var str = ""; for(var i = 0; i < arr.length; i++){ if(i > 0){ str += connector; } if(arr[i] !== undefined){ str += arr[i]; } } return str; } console.log(fakeJoin(a,"-"));//--

由上面的示例代码可以得出,稀疏数组所表现的视觉上的undefined并不是真正数组元素就是undefined,我们可以把它理解为一种数组空元素的类型转换,其并具备undefined的操作特性,而是保持数组空元素的纯粹空的特性,不具备被访问操作的能力。而join方法的操作我已经在示例中给出仿写方法的代码,其内部并不具操作元素值的行为,所以表现上有所差别,这个示例非常合适说明了数组纯空元素的特性,一种是访问值获得的是undefined,另一种是在纯空元素的值上操作是不成立的。

b.时间对象Date(...)和错误提示对象Error(...)

 因为Date(...)和Error(...)没有对应的常量,相对其他原生函数来说就正常多了。这里就简单的展示一下语法:

//三种实例化时间对象的语法
var date = new Date();
var date1 = Date();
var date2 = Date(date);
console.log(date == date1);//true
//两种获得距离1970年1月1号之间的毫秒数
console.log(Date.now());//ES5
console.log(date.getTime());

兼容ES5以前版本浏览器的Date.now():

if(!Date.now){
    Date.now = function(){
        return (new Date()).getTime();
    }
}

关于Error对象就不多做解释了,因为我们的开发中并不会有可能自己写Error对象,我直接复制了手册的一套代码:以下实例中 try 语句块包含了未定义的函数 "adddlert" ,执行它会产生错误,catch 语句块会输出该错误的信息:

try {
    adddlert("Welcome");
}
catch(err) {
    document.getElementById("demo").innerHTML = 
    err.name + "<br>" + err.message;
}
//如果你要测试这套代码,一定记得先定义一个Id为demo的元素

关于Symbol(...)原生函数就放到ES6部分详细介绍,Object和function原生函数也不在这里介绍了,会有具体针对这两个对象解析的博客。

这篇博客主要是为了做类型转换详细剖析而铺垫,内容不难,但涉及了很多语法特性的细节,还是慎重对待吧。最后一个重磅级不难的知识点,但有时候会被混淆的Object.toString()这个API,准确的说应该是Object.prototype.toString();

四、toString()

这个方法对于我们判断值类型有着至关重要的帮助,但是很多基本类型对象上都自己重写了这个方法,而原始值类型使用属性调用的方式时又会隐式的进行基本类型对象转换来实现,但是又不会所有原始值类型都有对应的基本类型对象,反正对于初学者来说这个方法挺折磨人的,反正我和我身边的程序员刚刚学习这个方法的时候都有这种感受。希望把下面这些执行结果罗列出来能对你又说帮助。

var a = "abc";
var b = 123;
var c = true;
var d = null;
var e;
function f(a){
    return a * 2;
}
var ff = new Function("a","return a * 2;");//与f的效果等同
var arr = ["a","b","c"];
var obj = {
    a:1,
    b:"abc",
    c:function(){}
}
var date = new Date();
var exp = new RegExp("\\b(?:" + name + ")+\\b","ig");

console.log(a.toString());//abc -- 隐式调用String()对象原型上的toString()
console.log(Object.prototype.toString.call(a));//[object String]

console.log(b.toString());//"123" -- 隐式调用Number()对象原型上的toString()
console.log(Object.prototype.toString.call(b));//[object Number]

console.log(c.toString());//"true" -- 隐式调用Boolean()对象原型上的toString()
console.log(Object.prototype.toString.call(c));//[object Boolean]

//console.log(d.toString());//报错 -- null没有基础类型对象,原始值类型null上更不会有任何方法和属性
console.log(Object.prototype.toString.call(d));//[object Null]

//console.log(e.toString());//报错 -- undefined没有基础类型对象,原始值类型undefined也不会有任何方法和属性
console.log(Object.prototype.toString.call(e));//[object Undefined]

console.log(f.toString());//"function f(a){    return a * 2;}" -- Function对象原型上有自己的toString()方法
console.log(Object.prototype.toString.call(f));//[object Function]
//--ff与f实质上没有区别,就不重复了

console.log(arr.toString());//"a,b,c" -- Array对象原型上也有自己的toString()方法
console.log(Object.prototype.toString.call(arr));//[object Array]

console.log(obj.toString());//[object Object]

console.log(date.toString());//Sun Jan 20 2019 05:42:17 GMT+0800 (中国标准时间) -- Date()对象原型也有自己的String()方法
console.log(Object.prototype.toString.call(date));//[object Date]

console.log(exp.toString());///\b(?:)+\b/gi -- RegExp()对象原型上也有自己的toString()方法
console.log(Object.prototype.toString.call(exp));//[object RegExp]

通常我们都是使用typeof来获取值类型,但是这个方法在有时候并不完全靠谱,例如null会放回object,还有Function和Array表现的异同也是令人费解,这些小问题构成了初学JavaScript的原罪,希望在这些简单的问题上你能顺利过关。(typeof遇到数组,对象,null返回的是object类型,其他类型可以正常返回对应的类型名称

至于Object.prototype.toString()这个方法打印的到底是什么,可以理解为值的一个内部属性[[Class]],然而ES5到ES6,toString()和[[Class]]的行为发生了一些改变,详细到ES6的博客中介绍。

 

posted @ 2019-01-19 06:57  他乡踏雪  阅读(213)  评论(0编辑  收藏  举报