Bootstrap

JavaScript 学习笔记——数据类型

类型分类

基本数据类型

  • Number

  • Boolean

  • String

  • Null

  • Undefined

  • Symbol

引用类型

  • 数组(Array)

  • 函数(Function)

  • 正则(RegExp)

  • 日期(Date)

其中引用类型可变,基本数据类型不可变。

怎么理解可变不可变?

  • 可变是指原始类型存储在栈里面,每个的大小固定,改变时就会重新开一个固定内存,而原地址会被回收。

  • 不可变是指引用类型存储在堆栈上可以动态改变大小。

类型判断

typeof

返回值:类型字符串

typeof本身是一个运算符,和加减乘除一样

var a = Symbol();
var c = function(){};
console.log(typeof(1));//number
console.log(typeof('sad'));//string
console.log(typeof(true));//boolean
console.log(typeof(a));//symbol
console.log(typeof(null));//object
console.log(typeof(undefined));//undefined
console.log(typeof([2]));//object
console.log(typeof({b:3}));//object
console.log(typeof(c));//function

由上可知,typeof 可区分number,string,boolean,symbol,undefined,object中的function。而数组等其他的对象类型不能区分。且null类型返回Object。

instanceof

返回值:布尔值,判断这个对象是否是这个特定类或者是它的子类的一个实例。

[] instanceof Array; //true
[] instanceof Object; //true

instanceof 只能判断对象,不能判断基本类型,因为基本类型没有原型。

constructor

判断实例对象是由哪个构造函数产生的

var f = new F();
f.constructor === F;// true

但易变,不可信赖,当重写后原有的会丢失

function F(){}
F.prototype={
    _name:'Eric',
};
var f = new F();
f.constructor === F;//false

所以在重写原型时需要给赋值

function F(){}
F.prototype={
    constructor:F,
    _name:'Eric',
};
var f = new F();
f.constructor === F;//true

Object.prototype.toString.call()

是原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲是运行时的指向的对象类型。

当 toString 方法被调用的时候,下面的步骤会被执行:

console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(null)) // [object Null]

var date = new Date();
console.log(Object.prototype.toString.call(date)) // [object Date]

这个 class 值就是识别对象类型的关键

需要注意的是,必须通过 来获取。因为大部分对象自身都实现了方法,这样就可能会导致的被终止查找,因此要用来强制执行的方法。

实现一个精确判断类型的函数

var class2type = {};

//把每个类型变成对象存在class2type中
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
    if (obj == null) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

有了 type 函数后,我们可以对常用的判断直接封装,比如 isFunction, isArray:

function isFunction(obj) {
    return type(obj) === "function";
}
 
function isArray(obj) {
    return type(obj) === "array";
}

复杂判断

plainObject

什么是 plainObject?

plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓"纯粹的对象",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。

我认为主要是用来区别是自定义构造函数和Object构造函数

function F(){
    console.log('1');
}
c = new F();
Object.prototype.toString.call(c)//"[object Object]"
//自定义构造函数Object.prototype.toString.call()也返回"[object Object]"

jQuery isPlainObject 的实现源码

var class2type = {};

// 相当于 Object.prototype.toString
var toString = class2type.toString;

// 相当于 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;

function isPlainObject(obj) {
    var proto, Ctor;

    // 排除掉明显不是obj的以及一些宿主对象如Window
    if (!obj || toString.call(obj) !== "[object Object]") {
        return false;
    }

    /**
     * getPrototypeOf es5 方法,获取 obj 的原型
     * 以 new Object 创建的对象为例的话
     * obj.__proto__ === Object.prototype
     */
    proto = Object.getPrototypeOf(obj);

    // 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
    if (!proto) {
        return true;
    }

    /**
     * 以下判断通过 new Object 方式创建的对象
     * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
     * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
     */
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;

    // 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
    return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}

这里需要区分一下:

Object.hasOwnProperty.toString.call(Ctor)和Object.prototype.toString.call(Ctor)

console.log(Object.hasOwnProperty.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]

因为是一个函数,所以调用的

而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。

所以源码最后一行代码中的  返回的是构造函数代码。

例如上面的  :

function F(){
    console.log('1');
}
c = new F();
Ctor = Object.getPrototypeOf(c).constructor
Object.hasOwnProperty.toString.call(Ctor)

/*
"function F(){
    console.log('1');
}"
*/

EmptyObject

jQuery isEmptyObject源码

function isEmptyObject( obj ) {

        var name;

        for ( name in obj ) {
            return false;
        }

        return true;
}

但是根据这个源码我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。

举个栗子:

console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true

isArrayLike

源码

function isArrayLike(obj) {

    // obj 必须有 length属性
    var length = !!obj && "length" in obj && obj.length;
    var typeRes = type(obj);

    // 排除掉函数和 Window 对象
    if (typeRes === "function" || isWindow(obj)) {
        return false;
    }

    return typeRes === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}

所以如果 isArrayLike 返回true,至少要满足三个条件之一:

第二个条件存在争议

var obj = {a: 1, b: 2, length: 0}
//这个对象用isArray返回true

之所以保留  是因为 arguments 是一个类数组对象

function a(){
    console.log(isArrayLike(arguments))
}
a();

这里应该返回true。

第三个条件为什么要 obj[length - 1] 必须存在?

因为数组 length 的长度是最后一个元素的 key 值加 1

数组2种写法:

var arr1 = [,,3]

//当我们写一个对应的类数组对象就是:
var arrLike = {
    2: 3,
    length: 3
}

var arr2 = [1,,];
console.log(arr.length) // 2

关于为什么arr2长度是2而不是3,可以看,JavaScript 忽略数组中的尾后逗号

var arrLike = {
    0: 1
}
console.log(arrLike.length)//undefined

var arrLike = {
    0: 1,
    length:1
}
console.log(arrLike.length)//1

总结

通过 isArrayLike 的第二种判断条件存在的问题,可以总结出:一些方法的实现也并不是非常完美和严密的,但是最后为什么这么做,其实也是一种权衡,权衡所失与所得。所有这些点,都必须脚踏实地在具体应用场景下去分析、去选择,要让场景说话。

类型转换

原始值转数字

方法:Number()

如果 Number 函数不传参数,返回 +0,如果有参数,调用 

让我们写几个例子验证一下:

console.log(Number()) // +0
console.log(Number(undefined)) // NaN
console.log(Number(null)) // +0
console.log(Number(false)) // +0
console.log(Number(true)) // 1
console.log(Number("123")) // 123
console.log(Number("-123")) // -123
console.log(Number("1.2")) // 1.2
console.log(Number("000123")) // 123
console.log(Number("-000123")) // -123
console.log(Number("0x11")) // 17
console.log(Number("")) // 0
console.log(Number(" ")) // 0
console.log(Number("123 123")) // NaN
console.log(Number("foo")) // NaN
console.log(Number("100a")) // NaN

如果通过 Number 转换函数传入一个字符串,它会试图将其转换成一个整数或浮点数,而且会忽略所有前导的 0,如果有一个字符不是数字,结果都会返回 NaN,鉴于这种严格的判断,我们一般还会使用更加灵活的 parseInt 和 parseFloat 进行转换。

parseInt 只解析整数,parseFloat 则可以解析整数和浮点数,如果字符串前缀是 "0x" 或者"0X",parseInt 将其解释为十六进制数,parseInt 和 parseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN:

console.log(parseInt("3 abc")) // 3
console.log(parseFloat("3.14 abc")) // 3.14
console.log(parseInt("-12.34")) // -12
console.log(parseInt("0xFF")) // 255
console.log(parseFloat(".1")) // 0.1
console.log(parseInt("0.1")) // 0

原始值转字符

方法:String()

如果  函数不传参数,返回空字符串,如果有参数,调用 

对象转字符串和数字

toString
Object.prototype.toString.call({a: 1}) // "[object Object]"
({a: 1}).toString() // "[object Object]"
({a: 1}).toString === Object.prototype.toString // true

我们可以看出当调用对象的 toString 方法时,其实调用的是 Object.prototype 上的 toString 方法。

然而 JavaScript 下的很多类根据各自的特点,定义了更多版本的 toString 方法。例如:

  • 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。

  • 函数的 toString 方法返回源代码字符串。

  • 日期的 toString 方法返回一个可读的日期和时间字符串。

  • RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。

console.log(({}).toString()) // [object Object]

console.log([].toString()) // ""
console.log([0].toString()) // 0
console.log([1, 2, 3].toString()) // 1,2,3
console.log((function(){var a = 1;}).toString()) // function (){var a = 1;}
console.log((/\d+/g).toString()) // /\d+/g
console.log((new Date(2010, 0, 1)).toString()) // Fri Jan 01 2010 00:00:00 GMT+0800 (CST)

转换规则

参数类型结果Object1. primValue = ToPrimitive(input, String)2. 返回ToString(primValue).

先调用一个 ToPrimitive 方法,将其转为基本类型,然后再参照“原始值转字符” 的对应表进行转换。

valueOf

表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

var date = new Date(2017, 4, 21);
console.log(date.valueOf()) // 1495296000000
转换规则:

参数类型结果Object1. primValue = ToPrimitive(input, Number)2. 返回ToNumber(primValue)。

先调用一个 ToPrimitive 方法,将其转为基本类型,然后再参照“原始值转数字”的对应表进行转换。

总结

对象转字符:

String():

1.如果对象有 toString 方法,就调用。如果返回一个原始值,则将这个原始值变成字符串并返回。

2.如果对象没有 toString 方法,或者这个方法并不返回一个原始值(比如重写了toString()),那么 JavaScript 会调用 valueOf 方法。如果存在这个方法,则 JavaScript 调用它。如果返回值是原始值,JavaScript 将这个值转换为字符串,并返回这个字符串的结果。

3.否则,JavaScript 无法从 toString 或者 valueOf 获得一个原始值,这时它将抛出一个类型错误异常。

对象转数字:

Number():

对象转数字的过程中,JavaScript 做了同样的事情,只是它会首先尝试 valueOf 方法

举个例子:

console.log(Number({})) // NaN
console.log(Number({a : 1})) // NaN
console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2, 3])) // NaN
console.log(Number(function(){var a = 1;})) // NaN
console.log(Number(/\d+/g)) // NaN
console.log(Number(new Date(2010, 0, 1))) // 1262275200000
console.log(Number(new Error('a'))) // NaN

注意,在这个例子中, 和 都返回了 0,而 却返回了一个 NaN。我们分析一下原因:

当我们 的时候,先调用 的 方法,此时返回 ,因为返回了一个对象而不是原始值,所以又调用了 方法,此时返回一个空字符串,接下来调用 这个规范上的方法,参照对应表,转换为 , 所以最后的结果为 。

而当我们 的时候,先调用 的 方法,此时返回 ,再调用 方法,此时返回 ,接下来调用 ,参照对应表,因为无法转换为数字,所以最后的结果为 。

JSON.stringify

值得一提的是:JSON.stringify() 方法可以将一个 JavaScript 值转换为一个 JSON 字符串,实现上也是调用了 toString 方法,也算是一种类型转换的方法。下面讲一讲JSON.stringify 的注意要点:

1.处理基本类型时,与使用toString基本相同,结果都是字符串,除了 undefined

console.log(JSON.stringify(null)) // null
console.log(JSON.stringify(undefined)) // undefined,注意这个undefined不是字符串的undefined
console.log(JSON.stringify(true)) // true
console.log(JSON.stringify(42)) // 42
console.log(JSON.stringify("42")) // "42"

2.布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。

JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"

3.undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时(以保证单元位置不变))。

JSON.stringify({x: undefined, y: Object, z: Symbol("")}); 
// "{}"
JSON.stringify([undefined, Object, Symbol("")]);          
// "[null,null,null]"

4.JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。

function replacer(key, value) {
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);
console.log(jsonString)
// {"week":45,"month":7}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
console.log(JSON.stringify(foo, ['week', 'month']));
// {"week":45,"month":7}

5.如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:

var obj = {
  foo: 'foo',
  toJSON: function () {
    return 'bar';
  }
};
JSON.stringify(obj);      // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'

隐式转换

一元操作符+
console.log(+'1');

当 + 运算符作为一元操作符的时候,查看 ,会调用 处理该值,相当于 ,最终结果返回数字 。

那么下面的这些结果呢?

console.log(+[]);//0
console.log(+['1']);//1
console.log(+['1', '2', '3']);//NaN
console.log(+{});//NaN
二元操作符+

当计算 value1 + value2时:

  • Null与数组:

console.log(null + 1);

按照规范的步骤进行分析:

接下来:

的结果为0,(回想上篇 Number(null)), 的结果为 1

所以, 相当于 ,最终的结果为数字 。

  • 数组与数组: 

console.log([] + []);

依然按照规范:

所以,相当于 ,最终的结果是空字符串。

  • 数组与对象:

console.log([] + {});
console.log({} + []);

按照规范:

所以, 相当于 ,最终的结果是 "[object Object]"。

下面的例子,可以按照示例类推出结果:

console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"
  • ==相等:

分类:

1.左右类型相同:

a.有一个type为Undefined或Null返回true

b.如果有一个type为Number:

i.另一个为NaN,则返回false

 

ii.2个数值相等,返回true

iv.分别为+0和-0,返回true

c.如果有一个是 String 则当两个完全相同才会true

d.如果有一个是Boolean 则都为ture或false,才返回true

f.两个引用同一对象时返回true

2.左右 type 不同时:

a.一个为null另一个为undefined,返回true

b.一个为Number另一个为String,将String用ToNumber()转换成Number后,再与Number类型比较

c.有一个是Boolean,将Boolean用ToNumber()转换成Number类型,再比较。

/*
true == '2' 就相当于 1 == '2' 就相当于 1 == 2,结果自然是 false。

所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == true 和 xx == false 的写法。

比如:
*/

// 不建议
if (a == true) {}

// 建议
if (a) {}
// 更好
if (!!a) {}

d.一个为StringNumber,另一个为Object,则把Object用ToPrimitive()转换成基本类型,再比较

e.返回false

深浅拷贝

为什么会出现深浅拷贝?

因为有些数组嵌套了数组或对象,于是该数组的深度不是1,拷贝的时候如果只是在第一层拷贝,那么就会出现一个属性引用一个对象地址,而不是真正拷贝,所以当第二层对象的值改变,引用同一对象的属性也会改变。

数组的浅拷贝

如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

比如:

var arr = ['old', 1, true, null, undefined];
var new_arr = arr.concat();
new_arr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(new_arr) // ["new", 1, true, null, undefined]

用 slice 可以这样做:

var new_arr = arr.slice();

但是如果数组嵌套了对象或者数组的话,比如:

var arr = [{old: 'old'}, ['old']];

var new_arr = arr.concat();

arr[0].old = 'new';
arr[1][0] = 'new';

console.log(arr) // [{old: 'new'}, ['new']]
console.log(new_arr) // [{old: 'new'}, ['new']]

我们会发现,无论是新数组还是旧数组都发生了变化,也就是说使用 concat 方法,克隆的并不彻底。

如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。

我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。

所以我们可以看出使用 concat 和 slice 是一种浅拷贝。

数组的深拷贝

那如何深拷贝一个数组呢?这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
var new_arr = JSON.parse( JSON.stringify(arr) );
console.log(new_arr);

是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数,我们做个试验:

var arr = [function(){
    console.log(a)
}, {
    b: function(){
        console.log(b)
    }
}]
var new_arr = JSON.parse(JSON.stringify(arr));
console.log(new_arr);
// [null, {…}]
//0: null
//1: {}
//length: 2
//__proto__: Array(0)

浅拷贝的实现

var shallowCopy = function(obj){
    if(typeof obj !== 'object') return;
  var newObj = obj instanceof Array ? [] : {};
  for(var key in obj){
     if (obj.hasOwnProperty(key)){
       newObj[key] = obj[key];
     }
  }
}

深拷贝实现

想要深拷贝,就要避免只在一层上面赋值,所以递归到最里面的一层值

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}

因为使用了递归,所以性能不如浅拷贝,还需在开发时考虑实际需求。

基于深拷贝思想实现合并两个或者更多的对象的内容到第一个对象中(extend)

var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;

//isPlainObject()用来判断target是否是plainObject
function isPlainObject(obj) {
    var proto, Ctor;
    if (!obj || toString.call(obj) !== "[object Object]") {
        return false;
    }
    proto = Object.getPrototypeOf(obj);
    if (!proto) {
        return true;
    }
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
    return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}


function extend() {
    // 默认不进行深拷贝
    var deep = false;
    var name, options, src, copy, clone, copyIsArray;
    var length = arguments.length;
    // 记录要复制的对象的下标
    var i = 1;
    // 第一个参数不传布尔值的情况下,target 默认是第一个参数
    var target = arguments[0] || {};
    // 如果第一个参数是布尔值,第二个参数是 target
    if (typeof target == 'boolean') {
        deep = target;
        target = arguments[i] || {};
        i++;
    }
    // 如果target不是对象,我们是无法进行复制的,所以设为 {}
    if (typeof target !== "object" && !isFunction(target)) {
        target = {};
    }

    // 循环遍历要复制的对象们
    for (; i < length; i++) {
        // 获取当前对象
        options = arguments[i];
        // 要求不能为空 避免 extend(a,,b) 这种情况
        if (options != null) {
            for (name in options) {
                // 目标属性值
                src = target[name];
                // 要复制的对象的属性值
                copy = options[name];

                // 解决循环引用
                if (target === copy) {
                    continue;
                }

                // 要递归的对象必须是 plainObject 或者数组
                if (deep && copy && (isPlainObject(copy) ||
                        (copyIsArray = Array.isArray(copy)))) {
                    // 要复制的对象属性值类型需要与目标属性值相同
                    if (copyIsArray) {
                        copyIsArray = false;
                        clone = src && Array.isArray(src) ? src : [];

                    } else {
                        clone = src && isPlainObject(src) ? src : {};
                    }

                    target[name] = extend(deep, clone, copy);

                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }

    return target;
};

文章参考:[冴羽的博客]()