JavaScript - 数组、类数组、类型检测

549 阅读21分钟

数组

声明方式

数组的声明方式有两种:

  • 字面量方式var arr = []
  • 通过数组构造函数构造数组:var arr = new Array()

以上两种没啥区别,注意:若在构造函数里面只写一个数字 new Array(5) 时这个数字不是第一个值是 5 的意思,而是新创建的这个数组长度是 5

var arr = new Array(10);
console.log(arr) // (10) [empty × 10]

读写操作

  • JS 的数组是弱数据类型的数组,不像其他语言那样严格
  • 不可以溢出读
  • 可以溢出写
var arr = [1,2];
console.log(arr[3]) // undefined
arr[5] = 5;
console.log(arr) // (6) [1, 2, empty × 3, 5]

常用方法

改变原数组

  • reverse:使数组倒序
  • push:在数组的末尾增加数据,数据类型、数量不限,返回增加后的数组长度
  • pop:从数组末尾删除一位数据,同时返回这个被删除的数据,没有参数
  • shift:从数组最前面删除一位数据,同时返回这个数据,没有参数
  • unshift:在数组最前面添加数据,和 push 一样的用法
  • splice
    • 这个方法是截取,有三个参数,第一个参数是截取开始的位置,第二个参数是截取的长度,第三个参数是一组数据,代表要在截取的位置添加的数据
    • 若不写第三个参数,这个方法就变成了在数组中删除数据的作用,删除并返回被删除的元素
    splice (从第几位开始, 截取多少长度,在切口处添加新的数据)
    
    const a = [1,2,3,4,5,6,7]; 
    a.splice(0, 3); // 从 0 开始删除 3 个元素返回 [1, 2, 3]  
    console.log(a); // [4, 5, 6, 7] 
    a.splice(-1, 1); // 从 -1 开始往后删除 1 个元素返回 [7] 
    console.log(a); // [1,2,3,4,5,6] 
    a.splice(0, 2, '添加的元素'); // 返回 [1, 2] 
    console.log(a); // ['添加的元素',3,4,5,6,7]
    
  • sort:对数组的元素进行排序,可以在这个方法中传入一个参数(一个函数),该函数可自定义排序规则,否则就按照 ASCII 码来排序

不改变原数组

  • concat:连接多个数组,返回新的数组

  • join:让数组的每一个数据以传入参数作为分隔符连接成字符串

    • 可用这个方法来进行大量字符串的连接工作,可以先放进数组里然后用 join 连接成字符串即可
    • 字符串中的 split 操作刚好和 join 操作相反,split 是把字符串以某种方式分割成数组
  • sliceslice(从该位开始截取, 截取到该位),返回选定元素

    • 一个参数时表示从该位开始到最后都截取
    • 不写参数时则是整个截取
  • filter:这个方法起过滤作用,它同样不会改变原数组,而是返回一个原数组的子集,同样会传递一个方法,每个元素都会调用这个方法,只有返回 true 的元素才会被添加到新数组里,返回 false 的则不会

  • some/every

    • 这两个方法是数组的逻辑判定,对数组使用指定的函数进行判定,返回 truefalse
    • every 是若每个元素经过传递的方法判定后均返回 true,则最后才返回 true
    • some 是只要有一个元素返回 true,则就返回 true
  • reduce:使用指定的函数将数组元素进行组合,最后变成一个值(从左向右)

    // total: 必需,初始值或计算结束后的返回值
    // currentValue: 必需,当前元素
    // currentIndex: 可选,当前元素的索引       
    // arr: 可选,当前元素所属的数组对象
    // initialValue: 可选,传递给函数的初始值,相当于 total 的初始值
    
    array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
    

    reduce 的用法可参考:js 之 reduce 的最全用法

  • map:可传递一个指定的方法,让数组中的每个元素都调用一遍这个方法,最后返回一个新数组,注意:map 方法最后有返回值

扩展:遍历数组的方式有哪些?

  • for

    标准的 for 循环语句也是最传统的循环语句

    var arr = [1,2,3,4,5];
    for(var i = 0; i < arr.length; i ++) {
      console.log(arr[i]);
    }
    

    这是最简单的一种遍历方式,也是使用频率最高的,性能较好,但还能优化

    var arr = [1,2,3,4,5];
    for(var i = 0, len = arr.length; i < len; i ++) {
      console.log(arr[i]);
    }
    

    这里使用临时变量将长度缓存起来,避免重复获取数组长度,尤其是当数组长度较大时优化效果更明显

  • forEach

    普通 forEach,对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素

    var arr = [10,20,30];
    var result = arr.forEach((item, index, arr) => {
      console.log(item);
    });
    console.log(result);
    // 10
    // 20
    // 30
    // undefined 该方法没有返回值
    

    数组自带的 forEach 循环,使用频率较高,实际上性能比普通 for 循环弱

    原型 forEach:由于 forEachArray 自带的,对于一些非这种类型的,无法直接使用(如 NodeList),因此才有这种变种,使用这个变种可以让类数组拥有 forEach 功能

    const nodes = document.querySelectorAll('div');
    Array.prototype.forEach.call(nodes, (item, index, arr) => {
      console.log(item);
    })
    

    实际上性能要比普通 forEach

  • for...in

    任意顺序遍历一个对象的除了 Symbol 以外的可枚举属性,包括继承的可枚举属性

    一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上的属性也能被遍历,像 ArrayObject 使用内置构造函数所创建的对象均会继承自 Object.prototypeString.prototype,不可枚举就不能遍历了

    var arr = [1,2,3,4,5];
    for(let i in arr) {
      // 这里的 i 是对象属性,即数组下标
      console.log(i, arr[i]);
    }
    // 0 1
    // 1 2
    // 2 3
    // 3 4
    // 4 5
    

    性能不怎么好

  • for...of(不能遍历对象)

    在可迭代对象(具有 Iterator 接口,如 ArrayMapSetStringarguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象

    let arr = ['前端', '后端'];
    for(let item of arr) {
      console.log(item);
    }
    
    // 遍历对象
    let person = {
      name: 'tn',
      age: 18,
      city: 'shenzhen'
    };
    for(let item of person) {
      console.log(item); // Uncaught TypeError: person is not iterable
    }
    
    // 发现它是不可以的,可以搭配 Object.keys 使用
    for(let item of Object.keys(person)) {
      console.log(person[item]); 
    }
    // tn
    // 18
    // shenzhen
    

    这种方式是 ES6 里用到的,性能要好于 for...in,但仍然比不上普通 for 循环

  • map

    只能遍历数组,不能中断,返回值是修改后的数组

    var arr = [1,2,3];
    const res = arr.map(item => {
      return item + 1;
    });
    console.log(arr); // [1, 2, 3]
    console.log(res); // [2, 3, 4]
    
  • every

    对数组中的每一元素运行给定函数,若该函数对每一项均返回 true,则返回 true,否则返回 false

    var arr = [10,30,25,64,18,3];
    var result = arr.every((item, index, arr) => {
      return item > 3;
    });
    console.log(result); // false
    
  • some

    对数组中的每一元素运行给定函数,若该函数有一项返回 true,则返回 true,所有项返回 false 才返回 false

    var arr = [10,30,25,64,18,3];
    var result = arr.some((item, index, arr) => {
      return item < 3;
    });
    console.log(result); // false
    
  • reduce

    该方法对数组中的每个元素执行提供的 reducer 函数(升序执行),将其结果汇总为单个返回值

    var arr = [1,2,3,4];
    var reducer = (accumulator, currentValue) => accumulator + currentValue;
    console.log(arr.reduce(reducer)); // 10
    
  • filter

    对数组中的每一项运行给定的函数,会返回满足该函数的项组成的数组

    var arr = [3,6,7,12,20,64,35];
    var result = arr.filter((item, index, arr) => {
      return item > 3;
    });
    console.log(result); // [6, 7, 12, 20, 64, 35]
    

性能测试

使用工具测试结果如下:

image.png

也可以自己用代码测试

结果分析:数组遍历各个方法速度,传统的 for 循环最快,for...in 最慢

for-len > for > for-of > forEach > map > for-in

JS 原生遍历方法的建议用法:

  • for 循环遍历数组
  • for...in 遍历对象
  • for...of 遍历类数组对象(ES6
  • Object.keys() 获取对象属性名的集合

为什么 for...in 会变慢?

  • 因为 for...in 语法是第一个能够迭代对象键的 JS 语句,循环对象键({})与在数组([])上进行循环不同
  • 引擎会执行一些额外的工作来跟踪已经迭代的属性,因此不建议使用 for...in 来遍历数组

map 和 forEach

定义

首先来看 MDN 上对 map 和 forEach 的定义:

  • forEach:针对每个元素执行提供的函数(executes a provided function once for each array element)
  • map:创建一个新的数组,其中每个元素由调用数组中的每个元素执行提供的函数得来(creates a new array with the results of calling a provided function on every element in the calling array)

forEach 是否会改变原数组

既然 map 不会改变原数组,那 forEach 呢?以前查 map 和 forEach 的区别时经常看到这样一句话:

forEach() 方法不会返回执行结果,而是 undefined,即 forEach() 会修改原来的数组,而 map() 方法会得到一个新的数组并返回

我的理解是使用 forEach 遍历一个数组,修改数组 item 的值就会改变原数组,但最近看到一些文章说 forEach 并不一定会改变原数组, 测试如下:

  • 原始数据类型 -> 不会改动原数组

    const arr = [1, 2, 3, 4];
    arr.forEach(item => {
      item = item * 3;
    })
    console.log(arr); // [1,2,3,4]
    
  • 引用类型 -> 类似对象数组可以改变

    const arr = [{ name: 'aa', age: 18 }, { name: 'bb', age: 20 } ]; 
    arr.forEach(item => { 
      if(item.name === 'aa') { 
        item.age = 25; 
      } 
    }) 
    console.log(arr); // [{name: "aa", age: 25}, {name: "bb", age: 20}]
    

此时若想要操作里面的基本数据类型,就用 arr[index] 的形式赋值改变即可

let arr = ['1',1,{'1': 1},true,2]
arr.forEach((item, index) => {
  arr[index] = 2;
});
console.log(arr); // [2, 2, 2, 2, 2]

原因:上面基本数据类型也被改变了,因为使用 forEach 方法时对于每个数据都创建了一个变量 item,操作的是 item 变量,对于基本数据类型 item 变量就是新创建的一个栈内存,item 变量改变并不影响基本原来地址的改变,而 item 变量对应的是引用数据类型时实际还是一个引用地址,操作它仍旧操作的是对应的堆内存

map 真的不会改变原数组吗

const arr = [1, 2, 3]
const result = arr.map(item => {
  item = item * 2;
  return item;
});
console.log('arr', arr);     // [1, 2, 3]
console.log('result', result);  // [2, 4, 6]

可以看到,item 虽然重新被赋值成了 item * 2,但最后打印结果显示原 arr 并没有改变。这似乎印证了 map 真的不会改变原数组。现在测试一下当数组元素为 引用类型 的情况:

const arr = [{ name: 'Tom', age: 16 }, { name: 'Aaron', age: 18 }, { name: 'Denny', age: 20 }]; 
const result = arr.map(item => { 
  item.age = item.age + 2; 
  return item; 
}); 
console.log('arr', arr); 
console.log('result', result);

得到的结果如下图,可以看到原数组也被改变了

image.png

通过上面的例子可以得出结论:map 不会改变原始数组 的说法并不严谨,而应该说当数组中元素是原始值类型时 map 不会改变原数组;是引用类型时则会改变原数组

  • map 方法体现的是数据不可变的思想,该思想认为所有的数据都是不能改变的,只能通过生成新的数据来达到修改的目的,因此直接对数组元素或对象属性进行操作的行为都是不可取的
  • 这种思想其实有很多好处,最直接的就是避免了数据的隐式修改,immutable.js 是实现数据不可变的一个库,可通过专属的 API 对引用类型进行操作,每次形成一个新的对象

因此正确的做法应该是:声明一个新变量来存储 map 的结果,而不是去修改原数组

const arr = [{ name: 'Tom', age: 16 }, { name: 'Aaron', age: 18 }, { name: 'Denny', age: 20 }]; 
const result = arr.map(item => ({ 
  ...item, 
  age: item.age + 2 
})); 
console.log('arr', arr); 
console.log('result', result);

image.png

forEach 和 map 不修改调用它的原数组本身,但可在 callback 执行时改变原数组

数组里的数据是如何引用的呢?

  • JS 的数据有基本数据类型和引用数据类型,同时引出堆内存和栈内存的概念
  • 对于基本数据类型,它们在栈内存中直接存储变量名和值
  • 而引用数据类型的真实数据存储在堆内存中,它在栈内存中存储的是变量名和堆内存的地址。一旦操作了引用数据类型,实际操作的是对象本身,所以数组里的数据相应改变

上面的测试都是修改原数组中某个对象元素的某个属性,若直接修改数组的某个对象呢?

const arr = [
  {
    name: 'aa',
    age: 18
  }, 
  {
    name: 'bb',
    age: 20
  }
];

// forEach
// 注意,改变单次循环整个 item 是无效的
arr.forEach(item => {
  if(item.name === 'aa') {
     item = {
       name: 'cc',
       age: 30
     };
  }
})
console.log(arr); // [{name: "aa", age: 18}, {name: "bb", age: 20}]

// map
const arr1 = arr.map(item => {
  item = {
    name: 'cc',
    age: 30
  }  
  return item;
})
console.log(arr1, arr);
// [{name: "cc", age: 30}, {name: "cc", age: 30}]
// [{name: "aa", age: 18}, {name: "bb", age: 20}]

这是因为不论是 forEach 还是 map,所传入的 item 都是原数组所对应的对象的地址,当修改 item 某一个属性后指向这个 item 对应的地址的所有对象都会改变。但若直接将 item 重新赋值,则会另开辟内存存放,那 item 就和原数组所对应的对象没有关系了,不论如何修改 item,都不会影响原数组

扩展阅读:forEach、map、filter、find、sort、some 等易错点整理

执行速度对比

jsPref 是一个非常好的网站用来比较不同的 JS 函数的执行速度,感兴趣的可以试试

哪个更好?

取决于想要做什么

forEach 适合于并不打算改变数据而只是想用数据做些事情,如存入数据库

let arr = ['a', 'b', 'c'];
arr.forEach((item) => {
  console.log(item);
})
// a
// b
// c

map 适合于要改变数据值时,它会返回一个新的数组,这样的优点在于可以使用复合(composition)(mapfilterreduce 等组合使用)来玩出更多的花样

let arr = [1, 2, 3, 4, 5];
let newArr = arr.map(num => num * 2).filter(num => num > 5);
console.log(newArr); // [6, 8, 10]

核心要点

  • 能用 forEach 做到的,map 同样可以,反过来也是如此
  • map 会分配内存空间存储新数组并返回,forEach 不会返回数组
  • forEach 允许 callback 更改原始数组的元素,map 返回新数组

类数组 ArrayLike

所谓类数组,就是指可以通过 索引属性 访问元素且拥有 length 属性的对象,没有数组的其他方法,如 pushforEachindexOf 等,一旦使用会报错

// 一个简单的类数组对象
const arrLike = {
  0: 'JavaScript',
  1: 'Java',
  2: 'Python',
  length: 3
}

所谓类数组对象与数组的性质相似,是因为类数组对象在访问、赋值、获取长度上的操作与数组是一致的

const arr = ['JavaScript', 'Java', 'Python']; 

// 访问 
console.log(arr[0]); // JavaScript 
console.log(arrLike[0]); // JavaScript 

// 赋值 
arr[0] = 'new name'; 
arrLike[0] = 'new name'; 

// 获取长度 
console.log(arr.length); // 3 
console.log(arrLike.length); // 3

类数组的精妙在于:它和 JS 原生的 Array 类似,但它是自由构建的。它来自开发者对 JS 对象的扩展,即对于它的原型 prototype 我们可以自由定义,而不会污染到 JS 原生的 Array

  • 举个例子:const nodeList = document.querySelectorAll("div"); 得到的这个 nodeList 就是一个类数组
  • nodeList[0] 可取到第一个子元素。但当我们用 console.log(nodeList instanceof Array) 则会返回 false,即它并不是数组的实例,即不是数组

arguments

经常会遇到各种类数组对象,最常见的便是 argumengsarguments 是一个经典的类数组对象,在函数体中定义了 Arguments 对象,其包含函数的参数和其它属性,以 arguments 变量来指代,如:

function fn(name, age, job) {
    console.log(arguments);
}
fn('tn', '18', '前端')

在控制台打印结果如图:

image.png

可以看到 arguments 中包含了 函数传递的参数lengthcallee 等属性

  • length 属性表示的是实参的长度,即调用函数时传入的参数个数
  • callee 属性则指向函数本身,可通过它来调用函数自身。在一些匿名函数或立即执行函数里进行递归调用函数本身时,由于该函数没有函数名,不能用函数名的方式调用,就可用 arguments.callee 来调用

类数组转换为数组

Array.prototype.slice.call(arguments)

若不传参数则就是返回原数组的一个拷贝

Array.prototype.slice.call(arrayLike).forEach(function(item, index){  
...
})

Array.prototype.slice.call(arguments) 相当于 Array.prototype.slice.call(arguments, 0)

  • 借用了数组原型中的 slice 方法,通过 call 显式绑定把一个数组(或类数组)的子集,作为一个数组返回
  • 当后面的作用对象是一个类数组时,就会把这个类数组对象转换为了一个新的数组,相当于赋予了 arguments 这个对象 slice 方法

除了使用 Array.prototype.slice.call(arguments),也可简单的使用 [].slice.call(arguments) 来代替

// 一个通用的转换函数
const toArray = (arrLike) => {
  try {
    return Array.prototype.slice.call(arrLike);
  } catch(e) {
    let arr = [];
    for(let i = 0, len = arrLike.length; i < len; i ++) {
      arr[i] = arrLike[i];
    }
    return arr;
  }
}

类数组只有索引值和长度,没有数组的各种方法,若要类数组调用数组的方法,可以使用 Array.prototype.method.call 来实现

const a = {'0':'a', '1':'b', '2':'c', length:3};  // 类数组
Array.prototype.join.call(a, '+');  // "a+b+c"
Array.prototype.slice.call(a, 0);   // ["a", "b", "c"]
Array.prototype.map.call(a, function(x) { 
  return x.toUpperCase();
}); // ['A','B','C']

Array.from

Array.from() 是 ES6 中新增的方法,可以将两类对象转为真正的数组:类数组对象可遍历对象(部署了 Iterator 接口的数据结构),包括 ES6 新增的数据结构 Set 和 Map

不考虑兼容性的情况下,只要有 length 属性的对象都可用此方法转换成数组

const arr = Array.from(arguments);

扩展运算符 ...

ES6 中的扩展运算符 ... 也能将某些数据结构转换成数组,这种数据结构必须有 遍历器接口(Symbol.iterator),若一个对象没有部署这个接口就无法转换

const args = [...arguments];

使用 for

使用 for 循环挨个将 arguments 对象中的内容复制给新数组

function toArray() {
  var args = [];
  for(var i = 1; i < arguments.length; i ++) {
    args.push(arguments[i]);
  }
  return args;
}

对象转换成数组

Array.from(object)

和上文提到的 Array.from(arguments) 类似,注意:

  • object 中必须有 length 属性,返回的数组长度取决于 length 长度 ,若没有 length 属性,则转换后的数组是一个空数组
  • key 值必须是 数值型或字符串型的数字,如 {1:"bar"}{"1":"bar"} 而不是 {"name":"bar"}
  • 给出的对象长度 length 必须大于最大 key 值,若 key 值大于 length,不在 Array.from 返回的浅拷贝数组里
// obj 没有 length 值
const obj = { 1: 'bar', 2: 42 }; 
Array.from(obj) //[]

// obj 有 length 值
const obj = { 1: 'bar', 2: 42, length: 4}; 
Array.from(obj) //[undefined, "bar", 42, undefined]

// obj 有 length 值,但 key 值不是数值
const obj = { name: 'bar', age: 42, length: 2}; 
Array.from(obj) //[undefined,undefined]

// obj 有 length 值,key 值是数值,且 key 值在 length 内
const obj = { 1: 'bar', 2: 42, length: 4}; 
Array.from(obj) //[undefined, "bar", 42, undefined]

// obj 有 length 值,key 值是数值且 key 值不在 length 内
const obj = { 8: 'bar', 6: 42 ,length: 4}; 
Array.from(obj) //[undefined, undefined, undefined, undefined]

Object.values(object)

Array.from 不同的是 Object.values 不需要 length 属性,返回一个对象所有可枚举属性值

//返回结果根据对象的 values 大小从小到大输出
const obj = { 100: 'a', 2: 'b', 7: 'c' };  
Object.values(obj);  // ["b", "c", "a"] 

Object.keys(object)

返回一个对象自身的可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致

//返回结果根据对象的 keys 大小从小到大输出
const obj = { 100: 'a', 2: 'b', 7: 'c' };  
Object.keys(obj); // ["2", "7", "100"] 

Object.entries(object)

返回一个给定对象自身可枚举属性的键值对数组

const obj16 = { foo: 'bar', baz: 42 }; 
Object.entries(obj16); // [["foo", "bar"], ["baz", 42]]

for…in

会遍历数组所有的可枚举属性,包括原型上的方法和属性

// 返回对象 key
function getObjKeys(obj) {
  let keys = []
  for(let prop in obj)
    keys.push(prop);
    return keys;
}

const obj = { foo: 'bar', baz: 42 }; 
console.log(getObjKeys(obj)); //  ["foo", "baz"]

// 返回对象 value
function getObjValues(obj) {
  let values = []
  for(let prop in obj)
    values.push(obj[prop]);
    return values;
}

const obj = { foo: 'bar', baz: 42 }; 
console.log(getObjValues(obj)); // ["bar", 42]

若不想遍历原型上的方法和属性,可在循环内部判断一下,hasOwnPropery 方法可以判断某属性是否是该对象的实例属性

for (var key in myObject) {
  if(myObject.hasOwnProperty(key)){
    console.log(key);
  }
}

JS 获取对象属性长度

const obj = { name: 'bar', age: 42}; 

// 获取可枚举属性的长度
Object.keys(obj).length

// 带有不可枚举属性
Object.getOwnPropertyNames(obj).length

总结

  • for…in 只遍历对象自身和继承的可枚举的属性
  • Object.keys() 返回对象自身的所有可枚举的属性的键名
  • JSON.stringify() 只串行化对象自身的可枚举的属性
  • Object.assign() 忽略 enumerablefalse 的属性,只拷贝对象自身的可枚举的属性

类型检测

ES5 中,有个 Array.isArray() 方法来检测是否是数组,在 ES5 之前要检测数据是否是数组类型还是有点麻烦的

typeof

数组、对象、null 均会返回 object,因此无法很好区分数组和对象

instanceof

instanceof 检测时,只要当前的构造函数的 prototype 属性出现在实例对象的原型链上(可以通过__proto__ 在原型链上找到它),检测出来的结果都是 true

语法:[实例对象] instanceof [构造函数]

var oDiv = document.getElementById("div1"); // HTMLDivElement -> HTMLElement -> Element -> Node -> EventTarget -> Object 
console.log(oDiv instanceof HTMLDivElement); // true 
console.log(oDiv instanceof Node); // true 
console.log(oDiv instanceof Object); // true  

console.log([] instanceof Array); // true 
console.log(/^$/ instanceof RegExp); // true 
console.log([] instanceof Object); // true

注意:基本数据类型的值是不能用 instanceof 来检测

console.log(1 instanceof Number); // false

以下可行是因为被封装成对象

const num = new Number(1); 
num instanceof Number; // true

constructor

constructor 的原理其实和 instanceof 有点像,也是基于面向对象和原型链的

一个实例对象若是一个构造函数的实例,那它原型上的 constructor 其实也就指向了这个构造函数,可以通过判断它的 constructor 来判断它是不是某个构造函数的实例

console.log([].constructor === Array); // true 
console.log([].constructor === Object); // false 

// constructor 可避免 instanceof 检测数组时用 Object 也是 true 的问题 
console.log({}.constructor === Object); // true 
console.log([].constructor === Object); // false

注意:使用constructor判断时,若原型上的 constructor 被修改了,这种检测可能就失效了

function a() {}
a.prototype = { x: 1 }

let b = new a();
b.constructor === a;  // false

上面为 false 的原因是:constructor 这个属性是 a.prototype 的属性,在给 a.prototype 赋值时覆盖了之前的整个 prototype,即覆盖了 a.prototype.constructor,这时压根就没有这个属性,若非要访问这个属性,只能去原型链上找,这时会找到 Object

a.prototype.constructor === Object; // true 
b.constructor === Object; // true

要避免这个问题,我们在给原型添加属性时最好不要整个覆盖,而是只添加需要的属性,上面的代码可改为:

a.prototype.x = 1;

若一定要整个覆盖,记得把 constructor 加回来

a.prototype = {
  constructor: a,
  x: 1
}

到现在为止它们是好用的,但它们存在潜藏问题:Web 浏览器中可能有多个窗口或窗体,每个窗体都有自己的 JS 环境和全局对象且每个全局对象有自己的构造函数,因此一个窗体中的对象将不可能是另外窗体中的构造函数的实例

如在 iframe 间来回传递数组,而 instanceof 不能跨帧。虽然窗体间的混淆并不常发生,但这个问题已经证明 constructorinstanceof 都不是真正可靠的检测数组类型的方法

Object.prototype.toString.call(value)

找到 Object 原型上的 toString 方法,执行且让方法中的 this 指向 valuevalue 就是要检测数据类型的值)

调用某个值的内置 toString() 方法在所有浏览器中都返回标准的字符串结果,对于数组来说返回的字符串为 "[object Array]",这个方法对识别内置对象都非常有效

Object.prototype.toString.call([]) === "[object Array]";

实现 is 系列检测函数

createValidType 函数使用闭包保存数据状态的特性,批量生成 is 系列函数

const dataType = { 
  '[object Null]': 'null', 
  '[object Undefined]': 'undefiend', 
  '[object Boolean]': 'boolean', 
  '[object Number]': 'number', 
  '[object String]': 'string', 
  '[object Function]': 'function', 
  '[object Array]': 'array', 
  '[object Date]': 'date', 
  '[object RegExp]': 'regexp', 
  '[object Object]': 'object', 
  '[object Error]': 'error' 
},
toString = Object.prototype.toString;

function type(obj) { 
  return dataType[toString.call(obj)]; 
}

// 生成 is 系列函数 
function createValidType() { 
  for(let p in dataType) { 
    const objType = p.slice(8, -1); 
    (function(objType) { 
      window['is' + objType] = function(obj) { 
        return type(obj) === objType.toLowerCase(); 
      } 
    })(objType) 
  } 
}

createValidType(); 
console.log(isObject({})); // true 
console.log(isDate(new Date())); // true 
console.log(isBoolean(false)); // true 
console.log(isString(1)); // false 
console.log(isError(1)); // false 
console.log(isError(new Error())); // true 
console.log(isArray([])); // true 
console.log(isArray(1)); // false 

// 同时也实现了 type 函数,用以检测数据类型 
console.log(type({})); // "object" 
console.log(type(new Date())); // "date" 
console.log(type(false)); // "boolean" 
console.log(type(1)); // "number" 
console.log(type(1)); // "number" 
console.log(type(new Error())); // "error" 
console.log(type([])); // "array" 
console.log(type(1)); // "number"