自檢清單-JavaScript基礎-變數和型別
又是一個來說說自檢清單的無名選手,說多了都是學習。參考來自大佬出的《一名【合格】前端工程師的自檢清單》,每個題目不是隻是簡單回答了就沒了,多的是想通過這幾道問題,能夠真的瞭解到它是怎麼來的,因為自己也是在慢慢學習,當做筆記了。
希望當你看到最後,再回頭看看自檢清單的問題,腦子裡知道它來自哪個知識點,也能答得上來。當然,我自己也是學習,如果有問題,麻煩評論call我。
今天是第一節JavaScript基礎的第一個小節變數和型別
好了,那我們就開始吧
1.JavaScript規定了幾種語言型別
(1)原始型別 Boolean
-Null
【(js中的資料在底層是以二進位制儲存,如果前三位為0,那麼就會判定為object,而null的所有都為0) 因此typeOf null 出現結果是Object】
-Undefined
-Number
-String
-Symbol
-Bigint
(2).引用型別 -物件Object
(包括普通物件Object, 陣列物件Array, 正則物件RegExp, 日期物件Date, 數字函式Math, 函式物件Function, )
2.JavaScript物件的底層資料結構是什麼
4.JavaScript中的變數在記憶體中的具體儲存形式
js基本型別資料都是直接按值儲存在棧中的
(Undefined、Null、不是new出來的布林、數字和字串),每種型別的資料佔用的記憶體空間的大小是確定的,並由系統自動分配和自動釋放。這樣帶來的好處就是,記憶體可以及時得到回收,相對於堆來說,更加容易管理記憶體空間。
js引用型別資料被儲存於堆中
(如物件、陣列、函式等,它們是通過拷貝和new出來的)。其實,說儲存於堆中,也不太準確,因為,引用型別的資料的地址指標是儲存於棧中的,當我們想要訪問引用型別的值的時候,需要先從棧中獲得物件的地址指標,然後,在通過地址指標找到堆中的所需要的資料。 結合程式碼與圖來理解
let a1 = 0; // 棧記憶體
let a2 = "this is string" // 棧記憶體
let a3 = null; // 棧記憶體
let b = { x: 10 }; // 變數b存在於棧中,{ x: 10 }作為物件存在於堆中
let c = [1,2,3]; // 變數c存在於棧中,[1,3]作為物件存在於堆中
複製程式碼
結合下面圖進行理解:
當我們要訪問堆記憶體中的引用資料型別時
-
從棧中獲取該物件的地址引用
-
再從堆記憶體中取得我們需要的資料
基本型別發生複製行為
let a = 20;
let b = a;
b = 30;
console.log(a); // 20
複製程式碼
結合下面圖進行理解:
在棧記憶體中的資料發生複製行為時,系統會自動為新的變數分配一個新值,最後這些變數都是相互獨立互不影響的 引用型別發生複製行為
let a = { x: 10,y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
複製程式碼
1、 引用型別的複製,同樣為新的變數b分配一個新的值,儲存在棧記憶體中,不同的是,這個值僅僅是引用型別的一個地址指標
2、 他們兩個指向同一個值,也就是地址指標相同,在堆記憶體中訪問到的具體物件實際上是同一個
3、 因此改變b.x時,a.x也發生了變化,這就是引用型別的特性
結合下圖理解:
總結:
5.基本型別對應的內建物件,以及他們之間的裝箱拆箱操作
內建物件:
(1)Object是 JavaScript 中所有物件的父物件
(2)資料封裝類物件:Object、Array、Boolean、Number 和 String
(3)其他物件:Function、Math、Date、RegExp、Error。
(4)特殊的基本包裝型別(String、Number、Boolean)
(5)arguments: 只存在於函式內部的一個類陣列物件
裝箱:
把基本資料型別轉化為對應的引用資料型別的操作,裝箱分為隱式裝箱和顯示裝箱
隱式裝箱
let a = 'sun'
let b = a.indexOf('s')
console.log(b)
複製程式碼
//上面的程式碼後臺實際的步驟
let a = new String('sun')
let b = a.indexOf('s')
a = null
複製程式碼
在上面的程式碼中,a是基本型別,它不是物件,不應該具有方法,js內部進行了一些列處理(裝箱), 使得它能夠呼叫方法。在這個基本型別上呼叫方法,其實是在這個基本型別物件上呼叫方法。這個基本型別的物件是臨時的,它只存在於方法呼叫那一行程式碼執行的瞬間,執行方法後立刻被銷燬。實現機制:
(1)建立String型別的一個例項;
(2)在例項上呼叫指定的方法;
(3)銷燬這個例項;
顯示裝箱
通過內建物件可以對Boolean、Object、String等可以對基本型別顯示裝箱
let a = new String('sun ')
複製程式碼
拆箱:
拆箱和裝箱相反,就是把引用型別轉化為基本型別的資料,通常通過引用型別的valueof()和toString()方法實現
let name = new String('sun')
let age = new Number(24)
console.log(typeof name)//Object
console.log(typeof age) //Object
console.log(typeof age.valueOf())//number 24 基本的數字型別
console.log(typeof name.valueOf())//string 'sun' 基本的字元型別
console.log(typeof age.toString())//string '24' 基本的字元型別
console.log(typeof name.toString())//string 'sun' 基本的字元型別
複製程式碼
6.理解值型別和引用型別
值型別就是常說的常量型別
7.null和undefined的區別
一、undefined 在使用var宣告變數但未對其加以初始化時,這個變數的值就是undefined,例如:
var message;
console.log(message == undefined);//true
複製程式碼
但是,包含undefined值的變數與尚未定義的變數還是不一樣的。例:
var message;
console.log(message == undefined);
複製程式碼
var message;
console.log(message);//undefined
console.log(age);//產生錯誤
複製程式碼
困惑的是:對未初始化和未宣告的變數執行typeof操作符都返回undefined值。這樣返回的合理性在於:無論哪種變數都不能執行真正的操作。
var message; //這個變數宣告之後預設取得了undefined值
// 下面這個變數沒有宣告
// var age
console.log(typeof message); //"undefined"
console.log(typeof age); //"undefined"
複製程式碼
二、null
從邏輯角度,null值表示一個空物件指標,而這也正是使用typeof操作符檢測null值時會返回"object"的原因。
如果定義的變數準備在將來用於儲存物件,應該將該變數初始化為null。
【1】JS判斷undefined
if(typeof(exp) == 'undefined'){
console.log("undefined");
}
複製程式碼
【2】JS判斷null
先寫錯誤的寫法
(1)exp為undefined時,也會得到與null相同的結果
var exp = null;
if(exp == null){
alert("is null");
}
複製程式碼
(2)exp為undefined或者數字零時,也會得到與null相同的結果
var exp = null;
if(!exp){
alert("is null");
}
複製程式碼
(3)exp為null時,typeof返回object
var exp = null;
if(typeof(exp) == "undefined"){
alert("is null");
}
複製程式碼
以上三種寫法都是不正確的,下面這種才是正確的
var exp = null;
if(!exp && typeof(exo)!="undefined" && exp!=0){
console.log("is null");
}
複製程式碼
8.至少可以說出三種判斷JavaScript資料型別的方式,以及他們的優缺點,如何準確的判斷陣列型別
判斷資料型別的方法一般可以通過:typeof、instanceof、constructor、toString四種常用方法
1、typeof:(可以對基本型別做出準確的判斷,但對於引用型別,用它就有點力不從心了)
typeof 返回一個表示資料型別的字串,返回結果包括:number、boolean、string、object、undefined、function等6種資料型別。
typeof 可以對JS基本資料型別做出準確的判斷(除了null),而對於引用型別返回的基本上都是object,其實返回object也沒有錯,因為所有物件的原型鏈最終都指向了Object,Object是所有物件的祖宗。
但當我們需要知道某個物件的具體型別時,typeof 就顯得有些力不從心了。
2、instanceof
判斷物件和建構函式在原型鏈上是否有關係,如果有關係,返回真,否則返回假
function Aaa(){
}
var a1 = new Aaa();
console.log(a1 instanceof Aaa); //true判斷a1和Aaa是否在同一個原型鏈上,是的話返回真,否則返回假
var arr = [];
console.log(arr instanceof Aaa);//false
複製程式碼
再來看一下
var str = 'hello';
console.log(str instanceof String);//false
var bool = true;
console.log(bool instanceof Boolean);//false
var num = 123;
console.log(num instanceof Number);//false
var nul = null;
console.log(nul instanceof Object);//false
var und = undefined;
console.log(und instanceof Object);//false
複製程式碼
//以下全輸出是true
var oDate = new Date();
console.log(oDate instanceof Date);
var json = {}
console.log(json instanceof Object);
var arr = [];
console.log(arr instanceof Array);
var reg = /a/;
console.log(reg instanceof RegExp);
var fun = function(){};
console.log(fun instanceof Function);
var err = new Error();
console.log(err instanceof Error);
複製程式碼
從上面的執行結果我們可以看到,基本資料型別是沒有檢測出他們的型別,但是我們使用下面的方式建立num、str、boolean,是可以檢測出型別的:
var num = new Number(123);
console.log(num instanceof Number);
var str = new String('abcd');
console.log(str instanceof String);
var boo = new Boolean(true);
console.log(boo instanceof Boolean)
複製程式碼
3、constructor:檢視物件對應的建構函式
constructor 在其對應物件的原型下面,是自動生成的。當我們寫一個建構函式的時候,程式會自動新增:建構函式名.prototype.constructor = 建構函式名
var str = 'hello';
console.log(str.constructor == String);//true
var bool = true;
console.log(bool.constructor == Boolean);//true
var num = 123;
console.log(num.constructor ==Number);//true
// var nul = null;
// alert(nul.constructor == Object);//報錯
//var und = undefined;
//alert(und.constructor == Object);//報錯
var oDate = new Date();
console.log(oDate.constructor == Date);//true
var json = {};
console.log(json.constructor == Object);//true
var arr = [];
console.log(arr.constructor == Array);//true
var reg = /a/;
console.log(reg.constructor == RegExp);//true
var fun = function(){};
console.log(fun.constructor ==Function);//true
var error = new Error();
console.log(error.constructor == Error);//true
複製程式碼
從上面的測試中我們可以看到,undefined和null是不能夠判斷出型別的,並且會報錯。因為null和undefined是無效的物件,因此是不會有constructor存在的
同時我們也需要注意到的是:使用constructor是不保險的,因為constructor屬性是可以被修改的,會導致檢測出的結果不正確
function Aaa(){
}
Aaa.prototype.constructor = Aaa;//程式可以自動新增,當我們寫個建構函式的時候,程式會自動新增這句程式碼
function BBB(){}
Aaa.prototype.constructor = BBB;//此時我們就修改了Aaa建構函式的指向問題
console.log(Aaa.construtor==Aaa);//false
複製程式碼
可以看出,constructor並沒有正確檢測出正確的建構函式
4、Object.prototype.toString
(可以說不管是什麼型別,它都可以立即判斷出)
toString是Object原型物件上的一個方法,該方法預設返回其呼叫者的具體型別,更嚴格的講,是 toString執行時this指向的物件型別
返回的型別
格式為[object xxx],xxx是具體的資料型別,其中包括:
String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,... 基本上所有物件的型別都可以通過這個方法獲取到。
從這個結果也可以看出,不管是什麼型別的,Object.prototype.toString.call();都可以判斷出其具體的型別。
接下來我們分析一下四種方法各自的優缺點
從上表中我們看到了,stanceof和constructor不能跨iframe,上面沒有細說,所以下面我們直接上例子嘍
例:跨頁面判斷是否是陣列
window.onload = function(){
var oF = document.createElement('iframe');
document.body.appendChild(oF);
var ifArray = window.frames[0].Array;
var arr = new ifArray();
console.log( arr.constructor == Array ); //false
console.log( arr instanceof Array ); //false
console.log( Object.prototype.toString.call(arr) == '[object Array]' ); //true
};
複製程式碼
從結果中可以看出,constructor和instanceof都沒有正確的判斷出型別,只有object.prototype.toString.call();正確判斷出了
9.可能發生隱式型別轉換的場景以及轉換原則,應如何避免或巧妙應用
(1)首先我們需要知道隱式轉換到底是什麼
在js中,當運算子在運算時,如果兩邊資料不統一,CPU就無法計算,這時我們編譯器會自動將運算子兩邊的資料做一個數據型別轉換,轉成一樣的資料型別再計算o 這種無需程式設計師手動轉換,而由編譯器自動轉換的方式就稱為隱式轉換
例如1 > "0"這行程式碼在js中並不會報錯,編譯器在運算子時會先把右邊的"0"轉成數字0`然後在比較大小
(2)之後我們應該要了解的就是如何進行隱式轉換,它的規則是怎樣的呢?總的來說一共三種:
- 轉成
string型別: +(字串連線符) 規則:
2..轉成number型別:++/--(自增自減運算子) + - * / %(算術運算子) > < >= <= == != === !=== (關係運算符)
規則:
- 轉成
boolean型別:!(邏輯非運算子) 規則:
(3)雖然它的轉換規則只有三種,但是它的坑卻很多
坑一:字串連線符與算術運算子隱式轉換規則混淆
例:
console.log(1 + "true");//1true
console.log(1 + true);//2
console.log(1 + undefined);//NaN
console.log(1 + null);//1
複製程式碼
在這裡需要解釋一下這個坑就是: 將字串連線符(+:只要+號兩邊有一邊是字串)與算術運算子(+:兩邊都是數字)的隱式轉換搞混淆
-
字串連線符:會把其他資料型別呼叫
String()方法轉成字串然後拼接 -
算術運算子:會把其他資料型別呼叫
Number()方法轉成數字然後做加法運算
然後我們再看上面的例子
+是字串連線符:String(1)+“true” = “1true”
+是算術運算子:1 + Number(true)=1+1=2
+是算術運算子:1 + Number( undefined)=1+NaN=NaN
+是算術運算子:1 + Number(null)=1+0=1
坑二:關係運算符:會把其他資料型別轉換成number之後再比較關係
console.log("2" > 10);//false
console.log("2" > "10");
console.log("abc" > "b");//false
console.log("abc" > "aad");
console.log(NaN == NaN);//false
console.log(NaN == undefined);//false
console.log(NaN == 0);//false
console.log(undefined == null);
console.log(undefined == undefined);
console.log(null == null);
複製程式碼
(1)第一種,當關系運算子兩邊有一邊是字串的時候,會將該資料型別使用Number()轉換,然後比較關係
Number(“2”)>10 false
(2) 第二種,當運算子兩邊都是字串的時候,此時同時轉成number然後比較關係
但這裡要注意的是,此時並不是按照Number()的形式轉成數字,而是按照字串對應的unicode編碼來轉成數字
使用下面這個方法可以檢視字串unicode編碼
字串.charCodeAt(字元下標,預設為0)
複製程式碼
因此可以得出答案:
“2”>“10”= “2”.charCodeAt()>“10”.charCodeAt() = 50>49=true
複製程式碼
注意:該函式預設是返回第一個字元的編碼,如果要查詢第二個字元可以傳參下標 (3)第三種 含有多個字元的字串從左至右依次比較
“abc”>“b” 先比較‘a’ 與‘b’
“a”.charCodeAt()<“b”.charCodeAt()
“abc”>“aad” 先比較‘a’ 與‘a’相同,再比較‘b’和‘a’
“a”.charCodeAt()<“b”.charCodeAt()
複製程式碼
(4)第四種,特殊情況(無視規則):如果資料型別是undefined和null,可得出固定的結果哦
console.log(undefined == null);//true
console.log(undefined == undefined);//true
console.log(null == null);//true
複製程式碼
(5) 第五種,特殊情況(無視規則):NaN與任何資料型別比較都是false
坑三:複雜資料型別在隱式轉換時會先轉成String,然後再轉成Number運算
首先原理分析:複雜型別轉number型別順序如下
1.先使用valueOf()方法獲取其原始值,如果原始值不是number型別,則使用toString 方法轉成string
2.再將string轉成number運算
例子:
console.log([1,2] == '1,2');//true
//原理:左邊先轉成string,然後右邊也是string,再使用unicode編碼運算
console.log([1,2].valueOf());//[1,2]
console.log([1,2].toString());//'1,2'
複製程式碼
var a = {};
console.log(a == "[object Object]");
//原理:console.log(a.valueOf().toString());//[object Object]
複製程式碼
看完上面兩個例子,然後我們來看看接下來的這個題目:
如何完善a,使得其正確列印1
var a = ???
if(a = 1 && a = 2 && a = 3){
console.log(1);
}
複製程式碼
分析:邏輯與運算一假則假,要想讓if分支語句小括號條件成立,則必須要讓a的值同時等於1,3.可能開始看覺得根本不可能實現,但是賦值資料型別會先呼叫valueOf()方法,然後轉成number運算
而物件的valueOf()方法是可以重寫的
因此我們的答案可以寫成這樣
var a = {
i: 0,valueOf: function(){
this.i++;
return this.i;//每呼叫一次,讓物件a的i屬性自增一次並且返回
}
}
if(a == 1 && a == 2 && a == 3){//每一次運算是都會呼叫一次a的valueOf()方法
console.log("1");
}
複製程式碼
坑四:邏輯非隱式轉換與關係運算符隱式轉換搞混淆
首先注意:o 空陣列的
toString()方法會得到空字串,而空物件的toString()方法會得到字串[object Object](注意第一個小寫o,第二個大寫O喲) 先上題:
//一級坑
console.log([] == 0);
console.log(![] == 0);
//二級坑
console.log([] == ![]);
console.log([] == []);
//三級坑
console.log({} == !{});
console.log({} == {});
複製程式碼
要解決上面的判斷,我們首先必須清楚的是:
1、關係運算符:將其他資料型別裝換成數字
2、邏輯非:將其他型別使用Boolean()轉換成布林型別
以下8種情況轉換為布林型別會得到false: 0,-0,NaN,undefined,null,(空字串),false,document.all()
除了以上8種情況之外的所有資料都會得到true
然後我們再完成上面的題
(1)關係運算符
-[].valueOf().toString()得到空字串(前面已經說了)
-
Number(‘’)==0成立
(2)邏輯非:本質是‘![]’邏輯非表示式結果與0比較關係
-
邏輯非優先順序高於關係運算符,
![]=false(空陣列轉布林得到true,然後取反得到false) -
false==0成立 (3)邏輯非:本質是‘空陣列[]’與‘![]’這個邏輯非表示式結果作比較
-
[].valueOf().toString()得到空字串 -
![]=false -
Number(‘’)==Number(false)成立都是0
console.log([] == ![]);//true
console.log([] == []);
//是因為引用型別資料儲存在堆中,棧中儲存的是地址,所以他們的結果是false
複製程式碼
(4)邏輯非:本質是‘空物件{}’與‘!{}’這個邏輯非表示式結果作比較
-
{}.valueOf().toString()得到字串‘[object Object]’ !{}=falseNumber(‘[object Object]’)==Number(false)
console.log({} == !{});//false
console.log({} == {});//flase
是因為引用型別資料儲存在堆中,棧中儲存的是地址,所以他們的結果是false
複製程式碼
10.出現小數精度丟失的原因,JavaScript可以儲存的最大數字、最大安全數字,JavaScript處理大數字的方法、避免精度丟失的方法
• 首先我們來看看小數精度丟失的典型問題
1. 兩個簡單的浮點數相加
console.log(0.1 + 0.2 != 0.3)// true
複製程式碼
2.出現的原因:
計算機的二進位制實現和位數限制有些數無法有限表示。就像一些無理數不能有限表示,如 圓周率 3.1415926...,1.3333... 等。JS 遵循 IEEE 754 規範,採用雙精度儲存(double precision),佔用 64 bit。
0.1 >> 0.0001 1001 1001 1001…(1001無限迴圈)
0.2 >> 0.0011 0011 0011 0011…(0011無限迴圈)
複製程式碼
此時只能模仿十進位制進行四捨五入了,但是二進位制只有 0 和 1 兩個,於是變為 0 舍 1 入。這即是計算機中部分浮點數運算時出現誤差,丟失精度的根本原因。 3.解決方案:
把小數放到位整數(乘倍數),再縮小回原來倍數(除倍數)
// 0.1 + 0.2
(0.1*10 + 0.2*10) / 10 == 0.3 // true
複製程式碼
然後我們來看看JS中的最大數字:
最大安全數字:
JavaScript中的基本資料類Number是雙精度浮點數,它可以表示的最大安全範圍是正負9007199254740991,也就是2的53次方減一,在瀏覽器控制檯分別輸入Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER可檢視對應的最大/小值
const max = Number.MAX_SAFE_INTEGER;
console.log(max);//9007199254740991
複製程式碼
const min = Number.MIN_SAFE_INTEGER;
console.log(min);//-9007199254740991
複製程式碼
將這個最大值加一,可以得到預期的結果:
console.log(max + 1);//9007199254740992
複製程式碼
但是,如果我們再次增加它,結果不再可以完全表示為JavaScript Number:
console.log(max + 2);//9007199254740992
複製程式碼
我們會發現max+1和max+2的結果一樣。只要我們在JavaScript中獲得這個特定的值,就無法判斷它是否準確。對安全整數範圍以外的整數(即從Number.MIN_SAFE_INTEGER到Number.MAX_SAFE_INTEGER)的任何計算可能會失去精度。
出於這個原因,我們只能依靠安全範圍內的數字整數值。
以及處理最大數字的方法:
因此我們推出BigInt:
BigInt是JavaScript中的一個新的原始型別,可以用任意精度表示整數。使用BigInt,即使超出JavaScript Number 的安全整數限制,也可以安全地儲存和操作大整數。
chrome 67+開始支BigInt,本文所有demo都是基於chrome 67。
要建立一個BigInt,在數字後面新增n字尾即可,例如,123變成123n。全域性BigInt(number)函式可以用來將Number轉換成BigInt。
換句話說igInt(123) ===123n。
讓我們用這種技術來解決我們之前遇到的問題:
console.log(BigInt(Number.MAX_SAFE_INTEGER) + 2n);
//9007199254740993n
複製程式碼
如果我們將兩個Number 相乘:
console.log(1234567890123456789 * 123);
//151851850485185200000
複製程式碼
檢視上面兩個數字,末尾分別是9和3,9*3=27,然而結果末尾卻是000,明顯是錯誤的,讓我們用BigInt代替:
console.log(1234567890123456789n * 123n);
//151851850485185185047n
複製程式碼
這次我們得到了正確的結果。
Number的安全整數限制不適用於BigInt。因此,BigInt我們可以執行正確的整數運算而不必擔心失去精度。
【寫到最後】
讓我們再來看看這幾個問題,不知道現在你對這幾個問題印象會不會深一點。希望再思考問題的同時思考的是它來源的知識點:
1.JavaScript規定了幾種語言型別
2.JavaScript物件的底層資料結構是什麼
4.JavaScript中的變數在記憶體中的具體儲存形式
5.基本型別對應的內建物件,以及他們之間的裝箱拆箱操作
6.理解值型別和引用型別
7.null和undefined的區別
8.至少可以說出三種判斷JavaScript資料型別的方式,以及他們的優缺點,如何準確的判斷陣列型別
9.可能發生隱式型別轉換的場景以及轉換原則,應如何避免或巧妙應用
10.出現小數精度丟失的原因,JavaScript可以儲存的最大數字、最大安全數字,JavaScript處理大數字的方法、避免精度丟失的方法
好的,今天就到這裡了,下期見!
