【學習筆記】JavaScript - 數組、類數組、類型檢測

數組的聲明方式

數組的聲明方式有兩種:前端

  • 字面量方式:var arr = []
  • 經過數組構造函數構造數組:var arr = new Array()

以上兩種沒啥區別,注意:若在構造函數裏面只寫一個數字 new Array(5) 時這個數字不是第一個值是 5 的意思,而是新建立的這個數組長度是 5node

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 同樣的用法
spliceweb

  • 這個方法是截取,有三個參數,第一個參數是截取開始的位置,第二個參數是截取的長度,第三個參數是一組數據,表明要在截取的位置添加的數據
  • 若不寫第三個參數,這個方法就變成了在數組中刪除數據的做用,刪除並返回被刪除的元素

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:讓數組的每個數據以傳入參數做爲分隔符鏈接成字符串markdown

  • 可用這個方法來進行大量字符串的鏈接工做,能夠先放進數組裏而後用 join 鏈接成字符串便可
  • 字符串中的 split 操做恰好和 join 操做相反,split 是把字符串以某種方式分割成數組

sliceslice(從該位開始截取, 截取到該位),返回選定元素數據結構

  • 一個參數時表示從該位開始到最後都截取
  • 不寫參數時則是整個截取

filter:這個方法起過濾做用,它一樣不會改變原數組,而是返回一個原數組的子集,一樣會傳遞一個方法,每一個元素都會調用這個方法,只有返回 true 的元素纔會被添加到新數組裏,返回 false 的則不會閉包

someevery函數

  • 這兩個方法是數組的邏輯斷定,對數組使用指定的函數進行斷定,返回 true 或 false
  • every 是若每一個元素通過傳遞的方法斷定以後都返回 true,則最後才返回 true
  • some 是隻要有一個元素返回 true,那麼就返回 true

reduce:使用指定的函數將數組元素進行組合,最後變成一個值post

  • 從左向右
/* total: 必需,初始值或計算結束後的返回值 currentValue: 必需,當前元素 currentIndex: 可選,當前元素的索引 arr: 可選,當前元素所屬的數組對象 initialValue: 可選,傳遞給函數的初始值,至關於 total 的初始值 */
array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
複製代碼

reduce 的用法可參考:juejin.cn/post/684490…

map:可傳遞一個指定的方法,讓數組中的每一個元素都調用一遍這個方法,最後返回一個新數組,注意:map 方法最後有返回值

map 和 forEach

既然 map 不會改變原數組,那 forEach 呢?之前查 mapforEach 的區別時常常看到這樣一句話:

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 不會改變原數組;是引用類型時則會改變原數組

(1)map 方法體現的是數據不可變的思想,該思想認爲全部的數據都是不能改變的,只能經過生成新的數據來達到修改的目的,所以直接對數組元素或對象屬性進行操做的行爲都是不可取的
(2)這種思想其實有不少好處,最直接的就是避免了數據的隱式修改,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

forEachmap 不修改調用它的原數組自己,可是能夠在 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等易錯點整理

類數組 ArrayLike

  • 所謂類數組,就是指能夠經過 索引屬性 訪問元素且擁有 length 屬性的對象,沒有數組的其餘方法,如 push、forEach、indexOf 等,一旦使用會報錯
// 一個簡單的類數組對象
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.fromArray.from()ES6 中新增的方法,能夠將兩類對象轉爲真正的數組:類數組對象和可遍歷對象(部署了 Iterator 接口的數據結構),包括 ES6 新增的數據結構 SetMap

不考慮兼容性的狀況下,只要有 length 屬性的對象均可用此方法轉換成數組

const arr = Array.from(arguments);
複製代碼

擴展運算符 ...ES6 中的擴展運算符 ... 也能將某些數據結構轉換成數組,這種數據結構必須有 遍歷器接口(Symbol.iterator),若一個對象沒有部署這個接口就沒法轉換

const args = [...arguments];
複製代碼

對象轉換成數組

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:數組和對象都會返回 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
複製代碼

如下可行是由於被封裝成對象,因此 true

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 函數,用以檢測數據類型

onsole.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"
複製代碼
相關文章
相關標籤/搜索