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