1. 程式人生 >前端設計 >自檢清單-JavaScript基礎-變數和型別

自檢清單-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基本型別資料都是直接按值儲存在棧中的

(UndefinedNull、不是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]作為物件存在於堆中
複製程式碼

結合下面圖進行理解:

當我們要訪問堆記憶體中的引用資料型別時

  1. 從棧中獲取該物件的地址引用

  2. 再從堆記憶體中取得我們需要的資料

基本型別發生複製行為

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)資料封裝類物件:ObjectArrayBooleanNumberString

(3)其他物件:FunctionMathDateRegExpError

(4)特殊的基本包裝型別(StringNumberBoolean)

(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)銷燬這個例項;

顯示裝箱

通過內建物件可以對BooleanObjectString等可以對基本型別顯示裝箱

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資料型別的方式,以及他們的優缺點,如何準確的判斷陣列型別

判斷資料型別的方法一般可以通過:typeofinstanceofconstructortoString四種常用方法

1、typeof:(可以對基本型別做出準確的判斷,但對於引用型別,用它就有點力不從心了)

typeof 返回一個表示資料型別的字串,返回結果包括:numberbooleanstringobjectundefinedfunction等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);
複製程式碼

從上面的執行結果我們可以看到,基本資料型別是沒有檢測出他們的型別,但是我們使用下面的方式建立numstrboolean,是可以檢測出型別的:

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
複製程式碼

從上面的測試中我們可以看到,undefinednull是不能夠判斷出型別的,並且會報錯。因為nullundefined是無效的物件,因此是不會有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

(可以說不管是什麼型別,它都可以立即判斷出)

toStringObject原型物件上的一個方法,該方法預設返回其呼叫者的具體型別,更嚴格的講,是 toString執行時this指向的物件型別

返回的型別 格式為[object xxx],xxx是具體的資料型別,其中包括: String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,... 基本上所有物件的型別都可以通過這個方法獲取到。

從這個結果也可以看出,不管是什麼型別的,Object.prototype.toString.call();都可以判斷出其具體的型別。 接下來我們分析一下四種方法各自的優缺點

從上表中我們看到了,stanceofconstructor不能跨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
  

    
};
複製程式碼

從結果中可以看出,constructorinstanceof都沒有正確的判斷出型別,只有object.prototype.toString.call();正確判斷出了

9.可能發生隱式型別轉換的場景以及轉換原則,應如何避免或巧妙應用

(1)首先我們需要知道隱式轉換到底是什麼

在js中,當運算子在運算時,如果兩邊資料不統一,CPU就無法計算,這時我們編譯器會自動將運算子兩邊的資料做一個數據型別轉換,轉成一樣的資料型別再計算o 這種無需程式設計師手動轉換,而由編譯器自動轉換的方式就稱為隱式轉換

例如1 > "0"這行程式碼在js中並不會報錯,編譯器在運算子時會先把右邊的"0"轉成數字0`然後在比較大小

(2)之後我們應該要了解的就是如何進行隱式轉換,它的規則是怎樣的呢?總的來說一共三種:

  1. 轉成string型別: +(字串連線符) 規則:

2..轉成number型別:++/--(自增自減運算子) + - * / %(算術運算子) > < >= <= == != === !=== (關係運算符)

規則:

  1. 轉成boolean型別:!(邏輯非運算子) 規則:

(3)雖然它的轉換規則只有三種,但是它的坑卻很多

坑一:字串連線符與算術運算子隱式轉換規則混淆

例:

console.log(1 + "true");//1true
console.log(1 + true);//2
console.log(1 + undefined);//NaN
console.log(1 + null);//1
複製程式碼

在這裡需要解釋一下這個坑就是: 將字串連線符(+:只要+號兩邊有一邊是字串)與算術運算子(+:兩邊都是數字)的隱式轉換搞混淆

  1. 字串連線符:會把其他資料型別呼叫String()方法轉成字串然後拼接

  2. 算術運算子:會把其他資料型別呼叫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)第四種,特殊情況(無視規則):如果資料型別是undefinednull,可得出固定的結果哦

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]’
  • !{}=false
  • Number(‘[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_INTEGERNumber.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+1max+2的結果一樣。只要我們在JavaScript中獲得這個特定的值,就無法判斷它是否準確。對安全整數範圍以外的整數(即從Number.MIN_SAFE_INTEGERNumber.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處理大數字的方法、避免精度丟失的方法

好的,今天就到這裡了,下期見!