- 型別判斷
我們先說一下JS的資料型別,我們一般說JS有六大資料型別(ES6以前)分別是:
- 基本資料型別
- Number
- String
- Boolean
- null
- undefined
- 引用資料型別
- object
在ES6中新增了Symbol資料型別。
有時我們需要知道資料的型別其判斷一些事情,我們經常會用typeof去判斷資料型別。
- 那麼typeof能判斷什麼型別呢?
- Number
- String
- Boolean
- object
- undefined
- function
就這幾個資料型別,但這些我們夠用嗎?或者說準確嗎?
我們沒有看到null那麼他是什麼型別呢?我們用typeof null
發現它是object
,是不是很奇怪,其實這是一個bug,但是這個bug是能修復的但是不能修復,因為null一般是用來表示一個物件是空的,有時我們用null來取消事件,我理解的它好像是一個佔位符,表示這個物件是空的。那為什麼不能修復呢?因為修復它好說,但是修復了它會帶來許多麻煩。
- 專業點說就是:不同的物件在底層都是用二進位制來表示,在JS中二進位制前三位都是0的就會判斷為object型別,因為null全是0所以會判斷null也是object型別。
我們來看一下typeof判斷的情況。
console.log(typeof(1)); //number
console.log(typeof("1")); // string
console.log(typeof(true)); // boolean
console.log(typeof({})); // object
console.log(typeof(function (){})); // function
console.log(typeof(null)); // object
console.log(typeof(undefined)); // undefined
複製程式碼
當我們想判斷一個物件那個是不是null或者是不是Date、RegExp等型別時會怎麼樣呢?我們發現都是object
,那我們有沒有辦法區分他們呢?
在這之前我先介紹一下Object.prototype.toString這個方法,我相信大家不陌生吧。它也能判斷資料型別,但是是這樣的。
console.log(Object.prototype.toString.call(1));
console.log(Object.prototype.toString.call("1"));
console.log(Object.prototype.toString.call(true));
console.log(Object.prototype.toString.call({}));
console.log(Object.prototype.toString.call(function (){}));
console.log(Object.prototype.toString.call(null));
console.log(Object.prototype.toString.call(undefined));
console.log(Object.prototype.toString.call(new RegExp()));
console.log(Object.prototype.toString.call(new Date()));
[object Number]
[object String]
[object Boolean]
[object Object]
[object Function]
[object Null]
[object Undefined]
[object RegExp]
[object Date]
複製程式碼
我們發現它比typeof高階點,能分辨的更準確,但是格式好像不是我們要的。
接下來進入主題,直接上程式碼。
//首先定義好資料型別。
let types = {
"[object Object]": "Object",
"[object RegExp]": "RegExp",
"[object Date]": "Date"
};
function type(regs) {
let result = typeof(regs); // 先獲取傳過來的引數
// 如果是物件在進行判斷,不是則返回。
if(result === 'object'){
if(regs === null){
return 'null';
}else{
return types[Object.prototype.toString.call(regs)];
}
}else{
return result;
}
}
console.log(type(1)); //number
console.log(type("1")); // string
console.log(type(true)); // boolean
console.log(type({})); // object
console.log(type(function (){})); // function
console.log(type(null)); // null
console.log(type(undefined)); // undefined
console.log(type(new RegExp())); //RegExp
console.log(type(new Date())); //Date
複製程式碼
- 物件克隆
我們經常會用到一個物件去做一些事情,可能有時候我們不想改變原有的資料。,時候我們就需要物件克隆了,你可能簡單的以為就是 = 就行了,那我們來看一看。
let obj = {
a: 1
}
let obj2 = obj;
console.log(obj); //{a: 1}
console.log(obj2); //{a: 1}
複製程式碼
我們看到複製過來了,這樣我們就可以隨便使用了。那我們來修改一下obj看看。
obj.a = 2;
console.log(obj); //{a: 2}
console.log(obj2); //{a: 2}
複製程式碼
發現都變成了{a: 2}
,是不是很奇怪。因為物件是引用型別的,他們賦值其實是賦的地址值,就是他們指向同一個地方,那麼我們應該怎麼做呢?你應該知道怎麼遍歷物件,我們就把它遍歷一遍再複製。看程式碼
let obj = {
a: 1
}
function clone(obj) {
let a = {};
for(let o in obj){
a[o] = obj[o]
}
return a;
}
let obj2 = clone(obj);
console.log(obj); //{a: 1}
console.log(obj2); //{a: 1}
obj.a = 2;
console.log(obj); //{a: 2}
console.log(obj2); //{a: 1}
複製程式碼
沒有改變,看來我們成功了,那這篇文章就到這了。呵呵,其實遠沒有,我們來看一下有沒有什麼問題。
- 當裡面的資料為引用型別時:
let obj = {
a: {
b: 1
},
c: 3
}
function clone(obj) {
let a = {};
for(let o in obj){
a[o] = obj[o]
}
return a;
}
let obj2 = clone(obj);
console.log(obj);
console.log(obj2);
obj.a .b = 2;
console.log(obj);
console.log(obj2);
複製程式碼
我們發現
又出問題了。- 如果你知道for...in你就會知道它的另一個錯誤。就是它會遍歷它原型上的可列舉屬性和非Symbol的屬性。那麼我們怎麼改善一下呢?現在介紹一下
hasOwnProperty
這個屬性,它就是判斷自身有沒有這個屬性,而不會去原型上找。
function clone(obj) {
let a = {};
for(let o in obj){
if(obj.hasOwnProperty(o)){
a[o] = obj[o];
}
}
return a;
}
複製程式碼
這個問題解決了,就差上一個了,我們接著用判斷資料的型別來判斷是否還需要複製的方法解決上一個問題。
let obj = {
a: {
b: 1,
d: {
e:[{f: 2}],
g: {
h:{
l: 5
}
}
}
},
c: 3
}
function deepClone(origin, target) {
let tar = target || {},
arr = "[object Array]",
str = Object.prototype.toString;
for(let o in origin){
if(origin.hasOwnProperty(o)){
// 如果是物件接著遞迴複製
if(typeof origin[o] === 'object'){
// 判斷是物件還是陣列
tar[o] = str.call(origin[o]) === arr ? [] : {};
deepClone(origin[o], tar[o]);
}else{
tar[o] = origin[o];
}
}
}
return tar;
}
let obj2 = deepClone(obj, {});
console.log(obj);
console.log(obj2);
obj.a.d.g.h.l = 6;
console.log(obj.a.d.g.h.l); //6
console.log(obj2.a.d.g.h.l); //5
複製程式碼
- 其實這還不是最終的深克隆,因為這一個也有它自己的問題,但是面對一般的情況應該沒問題,跟高階的用法請自行學習。
- 模擬實現JQ的$.extend()方法(只是粗略的寫了一下,如有錯誤歡迎指出):
function extend() {
let origin, // 要拷貝的源
target = arguments[0], // 獲取第一個引數
isDeepClone = false; // 是否深拷貝
length = arguments.length, //拷貝的個數
arr = "[object Array]",
str = Object.prototype.toString,
i = 0;
if(typeof target === 'boolean'){
isDeepClone = target;
i ++;
target = arguments[i]; //獲取目標元素
}
//防止迴圈引用
if(origin === target){
return;
}
// 相容function
if(typeof target !== 'object' && typeof target !== 'function' ){
target = {};
}
for ( ; i < length; i++) {
origin = arguments[i];
for(let o in origin){
if(origin.hasOwnProperty(o)){
if(origin[o] === 'object'){
if(isDeepClone){
target[o] = str.call(origin[o]) === arr ? [] : {};
extend(true, target[o], origin[o]);
}
}else{
target[o] = origin[o];
}
}
}
}
return target;
}
複製程式碼
- 補充:其實不止這一種深克隆的方法,不如我們處理資料最常使用的JSON
let obj = {
a: {
b: function (argument) {
},
d: {
e:[{f: 2}],
g: {
h:{
l: 5
}
}
}
},
c: 3
}
let r = JSON.stringify(obj);
r = JSON.parse(r);
obj.a.d.g.h.l = 6;
console.log(r.a.d.g.h.l); // 5
複製程式碼
也是可以的,我們輸出一下r看看。
有沒有發現少了什麼?對,就是function,它不僅不能複製function還有undefined也不行,還有別的自己查一下吧。3.陣列克隆
有了上面的鋪墊,我們知道陣列也是引用型別,就不能簡單的等於來複制。
- concat
let arr = [8,5,6,6,8];
let arr2 = arr.concat();
arr2[3] = 1;
console.log(arr); //[8, 5, 6, 6, 8]
console.log(arr2); //[8, 5, 6, 1, 8]
複製程式碼
可以複製成功,那麼引用型別呢?
let arr = [8,{a: 1},6,6,8];
let arr2 = arr.concat();
arr2[1].a = 2;
console.log(arr);
console.log(arr2);
複製程式碼
- 還有我們常用的slice也是一樣
let arr = [8,{a: 1},6,6,8];
let arr2 = arr.slice();
arr2[1].a = 2;
arr2[2] = 2;
console.log(arr);
console.log(arr2);
複製程式碼
還有一些別的方法,我就不一一列舉了,這些都是淺複製。
如果想深度克隆陣列,也可以使用上面介紹的使用JSON也是可以的。
let arr = [8,{a: 1},6,6,8];
let arr2 = JSON.parse( JSON.stringify(arr) );
arr2[1].a = 2;
arr2[2] = 2;
console.log(arr);
console.log(arr2);
複製程式碼
目前想到的就這些,總感覺拉下了什麼,如果我想起來了我會繼續補充的。
4.閒聊
上面寫的我意猶未盡,可能是自己知識的侷限性暫時只能想到那些,上面說到了for...in,那麼我們來簡單的說一下for...of和它的區別。
- 他們都是遍歷用的,每次遍歷陣列和物件都會想起它們,那麼你會不會弄混呢。 那我們直接遍歷一次,看看有什麼區別。
let arr = [8,{a: 1},6,6,8];
let a = {
b:1,
r: 8,
h:{
e:6
}
}
console.log('for...of');
for(let i of arr){
console.log(i);
}
console.log('for...in');
for(let i in a){
console.log('key:' + i);
}
複製程式碼
是不是感覺挺好的,我們再來看看。
let arr = [8,{a: 1},6,6,8];
let a = {
b:1,
r: 8,
h:{
e:6
}
}
console.log('for...in遍歷陣列');
for(let i in arr){
console.log(i);
}
console.log('for...of遍歷物件');
for(let i of a){
console.log('key:' + i);
}
複製程式碼
- for...of遍歷物件直接報錯了,你有沒有注意到報錯的資訊。就是不可遍歷,因為不是iterable。陣列、字串、Set、Map,內建好了Iterator(迭代器),它們的原型中都有一個Symbol.iterator方法,而Object物件並沒有實現這個介面,使得它無法被for...of遍歷。
- 至於for...of遍歷物件就需要實現Iterator,這裡我就不寫了,百度好多。
- for...in遍歷陣列遍歷出來的是索引。不過我們也可以得到陣列的值。
for(let i in arr){
console.log(arr[i]); //[8,{a: 1},6,6,8]
}
複製程式碼
我覺得你看到這裡應該知道遍歷什麼用哪個更合適了。
- 補充
__proto__
的實現
Object.defineProperty(Object.prototype, __proto__, { get: function(){ return Object.getPrototypeOf(this); }, set: function(ob){ Object.setPrototypeOf(this, ob); return ob; } }) 複製程式碼
下一篇文章我想說一下陣列去重好像不是最全的陣列去重方法,因為內容挺多,我就不一起寫了,喜歡的可以點一個贊,或者關注一下。鼓勵一下一名自學前端的大學生。