你不知道的JS系列——你所忽略的細節

前言


若是你看完以爲沒用,歡迎打死兔子(博主)。javascript

1. parseInt()toString() 的可選參數

parseInt(string, radix)
radix 可選。表示要解析的數字的基數。該值介於 2 ~ 36 之間。 若是省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。若是它以 「0x」 或 「0X」 開頭,將以 16 爲基數。若是該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN。java

eg:
parseInt('10', 2);    // 2 1*2^1 + 0*2^0 = 2
parseInt('17', 8);    // 15 1*8^1 + 7*8^0 = 15
parseInt('1f', 16);   // 31 1*16^1 + 15*16^0 31
parseInt('0x12');     // 18 1*16^1 + 2*16^0 = 18
複製代碼

number.toString(radix)
radix 可選。規定表示數字的基數,是 2 ~ 36 之間的整數。若省略該參數,則使用基數 10。可是要注意,若是該參數是 10 之外的其餘值,則 ECMAScript 標準容許實現返回任意值。數組

eg:
var num = 15;
num.toString(2);    // 1111 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0 = 15
num.toString(8);    // 17 1*8^1 + 7*8^0 = 15
num.toString(16);   // f f在16進制中即表明15(0~9,A~F)
複製代碼

2. 1.toString() 報錯,爲什麼不輸出 "1" ?

var num = 1;
num.toString();  // "1"
Number(1).toString();   // "1"
1.toString();    // Uncaught SyntaxError: Invalid or unexpected token
1.0.toString();  // "1"
1..toString();   // "1"
1 .toString();   // "1"
複製代碼

緣由:當點跟在一個數字後面就意味着這個數字是一個浮點數,在點後面JS等待着一個數字。 因此在調用toString()以前,咱們須要告訴JS這是就是咱們要的數字。經過變量的形式調用toString(),其實是發生了隱式轉換,number屬於基本類型,是沒有toString()方法的,隱式轉換變爲包裝類型,因此不會報錯。安全

3. 爲什麼 0.1+0.2=0.30000000000000004

解答:對於計算機而言,兩個數字在相加時是以二進制形式進行的,在呈現結果時才轉換成十進制。這樣的結果是由於在轉換的過程當中發生了精度丟失,解決的辦法是先轉換爲整數執行操做後再降下來,固然對於複雜的,能夠引入第三方庫(bignumber.js等)。ide

Number.MAX_VALUE;   // 1.7976931348623157e+308
Number.MIN_VALUE;   // 5e-324
複製代碼

4. 數組經常使用迭代方法的第二參數

這裏的迭代方法有: every()filter()forEach()map()some()find()findIndex()
這些方法的所需參數都相同,以用的最多的forEach()爲例進行說明:函數

array.forEach(function(currentValue, index, arr), thisValue)
thisValue 可選。傳遞給函數的值通常用 "this" 值。若是這個參數爲空, "undefined" 會傳遞給 "this" 值post

eg:
function run(param) { 
    console.log( param, this.id ); 
}
var obj = { 
    id: "welcome"
}; 
[1, 2, 3].forEach( run, obj );  // 1 "welcome" 2 "welcome" 3 "welcome"
複製代碼

若是須要在迭代中顯示綁定this,不妨考慮下這個第二參數。ui

5. 數組最易被忽略的 reduce() 方法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
①function(total,currentValue, index,arr)this

  • total 必需。初始值, 或者計算結束後的返回值。
  • currentValue 必需。當前元素
  • currentIndex 可選。當前元素的索引
  • arr 可選。當前元素所屬的數組對象。

②initialValue 可選。傳遞給函數的初始值spa

妙用——統計字符串中每一個字符出現的次數,最簡練的代碼:
var str = 'welcome to my world';
var obj = {};
[...str].reduce((prev, cur)=>{
    obj[cur] ? obj[cur]++ : obj[cur] = 1;
}, {})
console.log(obj);   // {" ": 3,c: 1,d: 1,e: 2,l: 2,m: 2,o: 3,r: 1,t: 1,w: 2,y: 1}
複製代碼

6. 理解數組是特殊的對象

以定義對象的形式來定義數組,以下:

var arr = {
    0: 1,
    1: 2,
    2: 3,
    length: 3
}
arr[0];     // 1
typeof arr;     // "object"
複製代碼

數組是特殊的對象,天然能夠添加屬性:

var arr = [1, 2, 3];
arr.md = 'well';
arr.length;     // 3
arr.md;         // "well"
// 注意點
arr["2"] = 4;   // 這裏有引號,本質上就算這裏沒有引號,也會先轉成字符串形式,由於對象的鍵只能爲字符串
arr[2];     // 4
複製代碼

7. 字符串的截取方法 slice()substr()substring() 的區別

string.slice(start,end) 參數能夠爲負數

string.substr(start,length) 第一參數可爲負數,第二參數爲長度

string.substring(from, to) 參數都必須爲非負整數

8. setTimeout() 第三參數

做用:第三參數會做爲第一個函數的參數傳入

對比下面代碼,見識一下做用:
for(var i = 0; i<6; i++){
    setTimeout(function(){
        console.log(i);
    },1000);
}               // 輸出6次6
for(var i=0;i<6;i++){
    setTimeout(function(j){
        console.log(j);
    },i*1000,i);
}               // 依次輸出0~5,間隔爲1秒
複製代碼

9. 你真的瞭解Javascript中的 && 操做符嗎?

  • 若是第一個操做數是對象,則返回第二個操做數;
  • 若是第二個操做數是對象,則只有在第一個操做數的求值結果爲 true 的狀況下才會返回該 對象;
  • 若是兩個操做數都是對象,則返回第二個操做數;
  • 若是有一個操做數是 null,則返回 null;
  • 若是有一個操做數是 NaN,則返回 NaN;
  • 若是有一個操做數是 undefined,則返回 undefined。
妙用:能夠看個人這篇《理解:有條件的對象屬性》

10. 你真的瞭解Javascript中的 + 操做符嗎?

undefined + undefined;      // NaN (Not a Number)
undefined + null;           // NaN
1 + null;                   // 1 說明:Number(null) 返回 0 isNaN(null) === false
1 + undefined;              // NaN 說明:Number(undefined) 返回 NaN isNaN(undefined) === true
'1' + null;                 // "1null"
'1' + undefined;            // "1undefined"
'1' + 1;                    // "11"
1+ {a:1}                    // "1[object Object]"
複製代碼

在《javaScript高級程序設計》一書"加性操做符"一節中,有很詳細的說明,這裏摘要三點:

  • 若是有一個操做數是 NaN,則結果是 NaN;
  • 若是隻有一個操做數是字符串,則將另外一個操做數轉換爲字符串,而後再將兩個字符串拼接 起來。

不過,若是有一個操做數是字符串,那麼就要應用以下規則:

  • 若是兩個操做數都是字符串,則將第二個操做數與第一個操做數拼接起來;
  • 若是隻有一個操做數是字符串,則將另外一個操做數轉換爲字符串,而後再將兩個字符串拼接 起來。
  • 若是有一個操做數是對象、數值或布爾值,則調用它們的 toString()方法取得相應的字符串值, 而後再應用前面關於字符串的規則。對於 undefined 和 null,則分別調用 String()函數並取得字符串"undefined"和"null"。

11. 你知道 ===== ,那你知道 Object.is() 與它們的區別嗎?

  • 相等運算符(==)會自動轉換數據類型
  • 嚴格相等運算符(===NaN 不等於自身,以及+0等於-0
眼見爲實:
1 == '1';       // true
1 === '1';      // false
NaN == NaN;     // false
NaN === NaN;    // false
+0 == -0;       // true
+0 === -0;      // true
null == undefined;      // true
null === undefined;     // false
複製代碼

Object.is() 與之不一樣之處:

  • +0不等於-0
  • NaN 等於自身
Object.is(+0, -0);     // false
Object.is(NaN, NaN);   // true
複製代碼

12. Object.create(null){} 區別

先看 Object.create() 的說明:
Object.create(proto[, propertiesObject])
參數:

proto 新建立對象的原型對象 propertiesObject 可選。若是沒有指定爲 undefined,則是要添加到新建立對象的不可枚舉(默認)屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)對象的屬性描述符以及相應的屬性名稱。這些屬性對應Object.defineProperties()的第二個參數

返回值:

一個新對象,帶着指定的原型對象和屬性。

Object.create(null) 將返回的新對象的原型設爲 null ,相比 {} ,它不會有 Object 對象原型上的如 toString() 等方法,它做爲空對象更乾淨更安全,不少時候用處很大。我喜歡用窮徒四壁來形容 {} ,那麼 Object.create(null) 對比之下就是連四壁也沒有。

13. ES6 屬性的遍歷及遍歷規則

  • for...in

for...in循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。

  • Object.keys(obj)

Object.keys返回一個數組,包括對象自身的(不含繼承的)全部可枚舉屬性(不含 Symbol 屬性)的鍵名。

  • Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一個數組,包含對象自身的全部屬性(不含 Symbol 屬性,可是包括不可枚舉屬性)的鍵名。

  • Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一個數組,包含對象自身的全部 Symbol 屬性的鍵名。

  • Reflect.ownKeys(obj)

Reflect.ownKeys返回一個數組,包含對象自身的(不含繼承的)全部鍵名,無論鍵名是 Symbol 或字符串,也不論是否可枚舉。

遍歷規則:

首先遍歷全部數值鍵,按照數值升序排列。

其次遍歷全部字符串鍵,按照加入時間升序排列。

最後遍歷全部 Symbol 鍵,按照加入時間升序排列。

eg:
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
複製代碼

14. 檢測對象是否具備某個屬性(你所忽略的 in 操做符)

共有3種方式:

  • if('key' in obj) 會檢測繼承屬性
  • obj.hasOwnProperty('key') 只檢測自身屬性
  • if(obj.key) 由於原型鏈機制,會檢測繼承屬性(對於if中發生的隱式轉換請閱讀標題15的內容)

注意: 慎用第三種方式,有時這種方式可能達不到你預期的效果。
舉例說明:

var myObject = { 
    a: undefined
};
myObject.a; // undefined 
myObject.b; // undefined
複製代碼

15. if 中隱式的類型轉換 Boolean()

數據類型 轉換爲true的值 轉換爲false的值
Boolean true false
String 任何非空字符串 ""(空字符串)
Number 任何非零數字值(包括無窮大) 0和NaN
Object 任何對象 null
Undefined 不適用 undefined

16. 對於內置函數 Date() 須要注意的地方

  • new Date().getDay() 返回的是一週中的某一天
  • new Date().getDate() 返回的是一月中的某一天

tips: 開發中總見有人對此混淆

17. 實現對象不變形的三種方式及其區別

  • Object.preventExtensions() 禁止擴展

禁止一個對象添加新屬性而且保留已有屬性,已有屬性能夠刪除

  • Object.seal() 密封

這個方法實際上會在一個現有對象上調用 Object.preventExtensions() 並把全部現有屬性標記爲 configurable:false。
因此,密封以後不只不能添加新屬性,也不能從新配置或者刪除任何現有屬性(雖然能夠 修改屬性的值)。

  • Object.freeze() 凍結

這個方法實際上會在一個現有對象上調用 Object.seal() 並把全部「數據訪問」屬性標記爲 writable:false,這樣就沒法修改它們的值。
這個方法是你能夠應用在對象上的級別最高的不可變性,它會禁止對於對象自己及其任意 直接屬性的修改

固然對應的還有解除不變性的方法,這裏就不介紹了。

18. 淺拷貝與深拷貝

淺拷貝和深拷貝都是相對於Object, Array這樣的引用類型而言的,由於對基本類型來講沒有意義

引用類型數據在內存中的存儲
棧內存 堆內存
name val val
obj 堆地址(指向堆內存的值) {a: 1,b: 2...}
很簡單的理解,淺拷貝就是隻拷貝了堆地址,而深拷貝是將堆內存中的值拷貝了一份。因此淺拷貝後,修改任何一個變量的值,都會在另外一個變量上獲得反映,是會相互影響的,深拷貝則不會。

實現淺拷貝的方式:

  • 常見的賦值操做
var a = [1, 2, 3];     
var b = a;
b[2] = 4;
console.log(a[2]);      // 4
複製代碼
  • Object.assign()
var obj = {
    a: 1,
    b: 2
}
var obj1 = Object.assign(obj);
obj1.a = 3;
console.log(obj.a) // 3
複製代碼

實現深拷貝的方式(排除第三方庫):

  • 利用 JSON 序列化
var obj = {
    a: 1,
    b: 2
}
var obj1 = JSON.parse(JSON.stringify(obj));
obj1.a = 3;
console.log(obj.a);     // 1
複製代碼
  • 擴展運算符
var arr = [1, 2, 3];
var arr1 = [...arr];
arr1[2] = 4;
console.log(arr[2]);    // 3
複製代碼
  • 對於數組的深拷貝,除擴展運算符外,還能夠利用數組自身的方法slice()、concat()

說明:slice()、concat()都會返回一個新的數組副本

  • 對於深層嵌套的複雜對象,採用遞歸賦值方式
function deepCopy(a, b= {}) {
    for(let item in a){
        if(typeof a[item] === "object"){
            b[item] = {};
            deepCopy(a[item], b[item]);
        }else{
            b[item] = a[item];
        }
    }
    return b;
}
var a = {
    x: 1,
    y: {
        z: 2
    }
}
var b = deepCopy(a);
a.y.z = 4;
console.log(b.y.z);    //2 b中屬性值並無改變,說明是深拷貝
複製代碼

19. 參數按 "值" 傳遞

解釋:傳入的參數,不管它是基本類型仍是引用類型,把握一個原則,按 "值" 傳遞。理解就是把函數外部值拷貝一份,而後把拷貝的值賦值給內部參數,這裏的拷貝特指淺拷貝,因此這裏拷貝的值分兩種狀況:

  • 基本類型參數:拷貝的值爲基本類型
  • 引用類型參數:拷貝的值爲內存中的地址引用,能夠理解爲一個指針指向
舉個以前博客的栗子,理解一下:
var type = 'images';
var size = {width800, height600};
var format = ['jpg', 'png'];
function change(type, size, format){
    type = 'video';
    size = {width1024, height768};
    format.push('map');
}
change(type, size, format);
console.log(type, size, format);    // 'images', {width: 800, height: 600}, ['jpg', 'png', 'map']
複製代碼

20. forforEach()map() 執行效率

for > forEach() > map()

緣由:forEach()map() 遍歷須要維護當前項和索引,必然要比 for 慢,而 map() 會分配內存空間存儲新數組並返回,forEach() 不會返回數據,因此 map() 最慢 。

21. super 關鍵字除了用在繼承時 class 類中構造函數裏面,還能用在哪裏?

  • 在繼承時,子類構造函數中的 super() 表明調用父類的構造函數,返回子類實例。super() 內部的 this 指的是子類的實例,所以 super() 在子類構造函數中至關於parentClass.prototype.constructor.call(this)。
  • super 做爲對象時,在普通方法中,指向父類的原型對象
class A {}
    A.prototype.x = 2;
    
    class B extends A {
      constructor() {
        super();
      }
      print(){
        console.log(super.x);
      }
    }

    var b = new B();
    b.print();      // 2
複製代碼
  • 在靜態方法中,指向父類
class A {       // 阮老師的例子
      constructor() {
        this.x = 1;
      }
      static print() {
        console.log(this.x);
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      static m() {
        super.print();
      }
    }
    
    B.x = 3;
    B.m() // 3
複製代碼

若是對最後一個程序心存疑惑,能夠去看個人這篇《你不知道的JS系列——全面解析this》

結語


小朋友,你在看到上面一個一個的標題的時候,是否有不少問號???
若是有,那證實基礎還需增強。

從此確定還會見到或想起同窗們所不太關注的知識點,此篇文章後續將進行不按期更新...

相關文章
相關標籤/搜索