Javascript數據類型知多少?
正如你所知道的,數據類型是作為js的入門知識點,在整個js的學習過程中也是尤為重要的。數據類型看起來簡單,但是圍繞著其衍生的邊界數據類型判斷問題、深拷貝淺拷貝問題對于新手而言是難以理解的。
一、數據類型
JavaScript 是一種弱類型或者說動態類型,這就意味著你不需要提前聲明變量的類型,在程序運行的過程中,類型會被自動確定。這就意味著你可以使用同一個變量保存不同類型的數據.
js內存分為棧內存(stack)和堆內存(heap)
- 棧內存:是一種特殊的線性表,它具有后進先出的特性,存放基本類型。
- 堆內存:存放引用類型(在棧內存中存一個基本類型值保存對象在堆內存中的地址,用于引用這個對象)。
數據類型根據存儲方式分為兩類:
- 基本數據類型(簡單數據類型、原始數據類型):值存儲在棧內存中,被引用或拷貝時,會創建一個完全相等的變量。占用空間小、大小固定,通過按值來訪問,屬于被頻繁使用的數據。
- 引用數據類型(復雜數據類型):地址存儲在棧內存中,值存在了堆內存中,多個引用會指向同一個地址。占據空間大、占用內存不固定。如果存儲在棧中,將會影響程序運行的性能;引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。
根據上面的標準劃分數據類型,常見的有:
- 基本數據類型:String、Number、Boolean、Undefined、Null、Symbol、BigInt
- 復雜數據類型:Object、Array、Date、Function、RegExp等
未命名文件 (1).png
二、數據類型的檢測
通常的數據類型的檢測有三種方法:
- typeof
- instanceof
2.1 typeof
使用typeof進行基礎數據類型(null除外)檢測,但是對于引用數據類型,除了function外,其它的均無法進行判斷。
- typeof "yichuan"; //"string"
- typeof 18; //"number"
- typeof undefined; //undefined
- typeof true; //boolean
- typeof Symbol(); //"symbol"
- typeof null; //"object"
- typeof []; //"object"
- typeof {}; //"object"
- typeof console; //"object"
- typeof console.log; //"function"
2.2 instanceof
使用instanceof是通過原型鏈進行查找,可以準確地判斷復雜引用數據類型,但是不能準確判斷基礎數據類型。
- let Fun = Function(){};
- let fun = new Fun();
- fun instanceof Fun;//true
- let str = new String("yichuan");
- str instanceof String;//true
- let str = "yichuan";
- str instanceof String;//false
2.3 Object.prototype.toString.call()
Object.prototype.toString方法返回對象的類型字符串,因此可用來判斷一個值的類型。因為實例對象有可能會自定義toString方法,會覆蓋Object.prototype.toString,所以在使用時,最好加上call。所有的數據類型都可以使用此方法進行檢測,且非常精準。
- Object.prototype.toString.call("yichuan");//["object String"]
- Object.prototype.toString.call(18);//["object Number"]
- Object.prototype.toString.call(true);//["object Boolean"]
- Object.prototype.toString.call(null);//["object Null"]
- Object.prototype.toString.call(new Symbol());//["object Symbol"]
- Object.prototype.toString.call({});//["object Object"]
- Object.prototype.toString.call([]);//["object Array"]
- Object.prototype.toString.call(/123/g);//["object RegExp"]
- Object.prototype.toString.call(function(){});//["object Function"]
- Object.prototype.toString.call(new Date());//["object Date"]
- Object.prototype.toString.call(document);//["object HTMLDocument"]
- Object.prototype.toString.call(window);//["object Window"]
我們可以看到此輸出的結果都是["object Xxxx"]首字母大寫。
2.4 通用的數據類型判斷方法
- function getType(obj){
- //先判斷輸入的數據判斷返回結果是否為object
- if(typeof obj !== "object"){
- return typeof obj;
- }
- // 對于typeof返回object的,再進行具體的判斷,使用正則返回結果,切記正則中間有個空格哦
- return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,"$1");
- }
切記:
- 使用typeof返回的類型是小寫
- 使用toString返回的類型是大寫
- getType("yichuna");//"string"
- getType(18);//"number"
- getType(true);//"boolean"
- getType(undefined);//"undefined"
- getType();//"undefined"
- getType(null);//"Null"
- getType({});//"Object"
- getType([]);//"Array"
- getType(function(){});//"Function"
- getType(new Date());//"Date"
- getType(/123/g);//"RegExp"
三、數據類型轉換
3.1 強制類型轉換
常見的強制類型轉換方法有:
- Number()
- String()
- Boolean()
- parseInt()
- parseFloat()
- toString()
3.2 Number()方法的強制轉換規則
- 布爾值 true和false分別被轉換為1和0
- 數字 返回本身
- null 返回0
- undefined 返回NaN
- 字符串
- 如果字符串中只包含數字,則將其轉換為十進制
- 如果字符串中只包含有有效的浮點格式,將其轉換為浮點數值
- 如果是空字符串,將其轉換為0
- 如果不是以上格式的字符串,則均返回NaN
- Symbol 拋出異常
3.3 Boolean()方法的強制轉換規則
undefined、null、false、""、0(包括+0、-0)、NaN轉換出來都是false,其余類型轉換都是true。特別注意:Boolean({})轉換為true
3.4 隱式類型轉換==
- 如果類型相同,無需進行類型轉換
- 如果其中一個操作值為null或undefined,那么另一個操作符必須是null或undefined才會返回true,否則均返回false
- 如果其中一個值是Symbol類型,那么返回false
- 如果其中一個操作知為Boolean,那么轉為number
- 兩個操作值均為string和number類型,那么將字符串轉為number
- 如果一個操作值為object,且另一個為string、number或symbol,就會把object轉為原始數據類型判斷
小試牛刀:
- null == undefined; //true
- null == 0;//false
- "" == null;//false
- "" == 0;//true 會轉為number類型再進行判斷
- "123" == 123;//true
- 0 == false;//true
- 1 == true;//true
3.5 隱式類型轉換+
"+"號操作符,不僅可以用于數字相加,還可以用于字符串拼接。
- 如果其中一個是字符串,另外一個是number、undefined、null或boolean,則調用toString()方法進行字符串拼接
- 如果是純字符串、數組、正則等,則默認調用對象的轉換方法會存在優先級,然后進行拼接
- 如果字符串和bigInt進行相加,會先將bigInt轉為字符串
- 如果number類型與undefined相加,則得到NaN
- 1 + 2;//3
- 1 + "2";//"12"
- "1" + undefined;//"1undefined"
- "1" + null;//"1null"
- "1" + true;//"1true"
- "1" + 1n;//"11" 字符串和bigInt進行相加,會先將bigInt轉為字符串
- 1 + undefined;//NaN undefined會先轉為NaN
- 1 + null;//1 null轉為0
- 1 + true;//2
- 1 + 1n;//Error
3.6 object的轉換規則
- 如果部署了Symbol.toPrimitive方法,優先調用再返回
- 調用valueOf(),如果轉換為基礎類型則返回
- 調用toString(),如果轉換為基礎數據類型則返回
- 如果都沒有返回基礎數據類型,則會報錯
四、深拷貝和淺拷貝
在js的編程中經常需要進行數據進行復制,那么什么時候使用深拷貝、什么時候使用淺拷貝呢,是開發過程中需要思考的?如何提升自己手寫js的能力,以及對一些邊界情況的深入思考能力呢?
有兩個重要問題:
- 拷貝一個很多嵌套的對象要如何實現呢?
- 深拷貝寫成什么程度才能讓面試官滿意呢?
4.1 淺拷貝的原理和實現
自己創建一個新的對象,來接受要重新復制或引用的對象值。
- 如果對象屬性是基本數據類型,復制的就是基本數據類型的值給新對象;
- 如果對象屬性是引用數據類型,賦值的則是內存中的地址,如果其中一個對象改變了這個內存中的地址,肯定會影響另外一個對象
4.1.1 Object.assign
Object.assign是es6中object的一個方法,該方法可以用于js對象的合并等多個用途,其中一個用途就是可以進行淺拷貝。
- Object.assign(target,...sources);//target目標對象,sources待拷貝的對象
注意:
- Object.assign不會拷貝對象的繼承屬性
- Object.assign不會拷貝對象的不可枚舉屬性
例如:
- let obj = {};
- let obj1 = {
- name:"yichuan",
- scores:{
- math:100,
- Chinese:100
- }
- };
- Object.assign(obj,obj1);
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:100}}
改變目標對象的值:我們可以看到下面改變了目標對象的值,會引起待拷貝對象的值的改變。
- let obj = {};
- let obj1 = {
- name:"yichuan",
- scores:{
- math:100,
- Chinese:100
- }
- };
- Object.assign(obj,obj1);
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
- obj.scores.Chinese = 10;
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
- console.log(obj1);//{name:"yichuan",scores:{math:100,Chinese:90}}
不可拷貝不可枚舉屬性
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- idCard:Symbol(1)
- };
- Object.defineProperty(obj1,"innumerable",{
- value:"不可枚舉屬性",
- enumerable:false
- });
- let obj2 = {};
- Object.assign(obj2,obj1);
- obj1.user.name = "onechuan";
- console.log("obj1",obj1);//{user: {…}, idCard: Symbol(1), innumerable: '不可枚舉屬性'}
- console.log("obj2",obj2);//{user: {…}, idCard: Symbol(1)} 我們可以看到并沒有innumerable屬性
4.1.2 展開運算符
- /* 對象的拷貝 */
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- school:"實驗小學"
- };
- let obj2 = {...obj1};
- obj2.school = "五道口男子技校";
- console.log(obj1);//{school: "實驗小學",user: {name: 'yichuan', age: 18}}
- obj2.user.age = 19;
- console.log(obj2);//{school: "實驗小學",user: {name: 'yichuan', age: 19}}
- /* 數組的拷貝 */
- let arr = ["red","green","blue"];
- let newArr = [...arr];
- console.log(arr);//['red', 'green', 'blue']
- console.log(newArr);//['red', 'green', 'blue']
4.1.3 concat拷貝數組
數組的concat方法其實也是淺拷貝
- let arr = ["red","green","blue"];
- let newArr = arr.concat();
- newArr[1] = "black";
- console.log(arr);//["red","green","blue"];
- console.log(newArr);//["red","black","blue"];
4.1.4 slice拷貝數組
slice方法僅針對數組類型,arr.slice(begin,end);
- let arr = ["red","green","blue"];
- let newArr = arr.slice();
- newArr[1] = "black";
- console.log(arr);//["red","green","blue"];
- console.log(newArr);//["red","black","blue"];
4.1.5 手寫淺拷貝
- 對基本數據類型進行最基本的拷貝
- 對引用數據類型開辟新的存儲,并且拷貝一層對象屬性
- function shallowClone(target){
- //先要判斷是否為對象數據類型
- if(typeof target === "object" && target !== null){
- //判斷輸入的是object類型還是數組類型
- const cloneTarget = Array.isArray(target) ?[]:{};
- //遍歷目標對象元素
- for(let prop in target){
- //判斷cloneTarget對象上是否有此屬性,沒有進行拷貝
- if(!cloneTarget.hasOwnProperty(prop)){
- cloneTarget[prop] = target[prop]
- }
- }
- return cloneTarget;
- }
- return target;
- }
4.2 深拷貝的原理和實現
前面我們知道淺拷貝只是創建了一個新的對象,復制了原有對象的基本類型的值。對于復雜引用數據類型,其在堆內存中完全開辟了一塊內存地址,并將原有對象完全復制過來存放。
深拷貝就是將一個對象從內存中完整地拷貝出來給目標對象,并在堆內存中開辟新的空間進行存儲新對象的值,且新對象的值改變不會影響原對象,也就是實現了二者的隔離。
4.2.1 JSON.stringify()
其實在實際開發過程使用最簡單的深拷貝就是使用JSON.stringify()配合JSON.parse()。但其實是有缺陷的,不影響簡單使用。注意:
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- school:"實驗小學"
- };
- let obj2 = JSON.parse(JSON.stringify(obj1));
- console.log(obj1);//{school: "實驗小學",user: {name: 'yichuan', age: 18}}
- console.log(obj2);//{school: "實驗小學",user: {name: 'yichuan', age: 18}}
- obj2.school = "門頭溝學員";
- obj2.user.age = 19;
- console.log(obj1);//{school: "實驗小學",user: {name: 'yichuan', age: 18}}
- console.log(obj2);//{school: "門頭溝學院",user: {name: 'yichuan', age: 19}}
4.2.2 簡易手寫深拷貝
作為簡易版手寫深拷貝,只能完成基礎的拷貝功能,也存在一些缺陷:
- 不能拷貝不可枚舉的屬性以及symbol類型
- 只能針對普通的引用類型的值做遞歸復制
- 對象的屬性里面成環,即循環引用沒有得到妥善解決
- function deepClone(obj){
- const cloneObj = {};
- //遍歷對象鍵名
- for(let key in obj){
- //判斷是否為對象類型
- if(typeof obj[key]==="object"){
- //是對象就再次調用函數進行遞歸拷貝
- cloneObj[key] = deepClone(obj[key]);
- }else{
- //是基本數據類型的話,就直接進行復制值
- cloneObj[key] = obj[key];
- }
- }
- return cloneObj;
- }
- const obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- school:"實驗小學"
- }
- let obj2 = deepClone(obj1);
- obj1.user.age = 19;
- console.log(obj2);//{school: "實驗小學",user: {name: 'yichuan', age: 18}}
4.2.3 優化版手寫深拷貝
對于上面簡易版的深拷貝,很顯然面試官是不買賬的,為此我們針對遞歸進行升級處理。
- 針對能夠遍歷對象的不可枚舉屬性以及Symbol類型,我們可以使用Reflect.ownKeys方法
- 當參數為Date、RegExp類型,則直接生成一個新的實例返回
- 利用Object的getOwnPropertyDescriptors方法可以獲得對象的所有屬性,以及對應的特性,順便結合Object.create()方法創建新對象,并繼承傳入原對象的原型鏈
- 利用WeakMap類型作為Hash表,因為WeakMap是弱引用類型,可以有效防止內存泄漏,作為檢測循環引用有很大的幫助。如果存在循環,則引用直接返回WeakMap存儲的值
- const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null;
- function deepClone(obj, hash = new WeakMap()) {
- //判斷是否為日期類型
- if (obj.constructor === Date) return new Date(obj);
- //判斷是否正則對象
- if (obj.constructor === RegExp) return new RegExp(obj);
- //如果循環引用了,就使用weakMap進行解決
- if (hash.has(obj)) return hash.get(obj);
- const allDesc = Object.getOwnPropertyDescriptors(obj);
- //遍歷傳入參數所有鍵的特性
- const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
- //繼承原型鏈
- hash.set(obj, cloneObj);
- for (const key of Reflect.ownKeys(obj)) {
- cloneObj[key] = isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key]) : obj[key];
- }
- return cloneObj;
- }
- const obj1 = {
- num: 2021,
- str: 'jue',
- bool: true,
- nul: null,
- arr: ['ref', 'green', 'blue'],
- date: new Date(0),
- reg: new RegExp('/123/g'),
- user: {
- name: 'yichuan',
- age: 18
- },
- school: '實驗小學'
- };
- const obj2 = deepClone(obj1);
- obj1.user.age = 19;
- console.log(obj2);//{arr: ['ref', 'green', 'blue'],bool: true,date: Thu Jan 01 1970 08:00:00 GMT+0800 (中國標準時間) ,nul: null,num: 2021,reg: /\/123\/g/,school: "實驗小學",str: "jue",user: {name: 'yichuan', age: 18}}
5參考學習
《如何寫出一個驚艷面試官的深拷貝?》
《JavaScript基本數據類型和引用數據類型》
《Javascript核心原理精講》
6寫在最后
其實在實際開發和使用過程中,很多人對于深拷貝的細節問題理解并不是很透徹,如果能夠更深層次的研究細節,你就會發現此部分內容對于了解更深層次js的底層原理有所幫助。這篇文章是作為對數據類型、數據類型的檢測、數據類型強制和隱藏轉換、深淺拷貝的簡要總結,希望對大家有所幫助。