本系列將從以下專題去總結:
1. JS基礎知識深入總結
2. 物件高階
3. 函式高階
4. 事件物件與事件機制
暫時會對以上四個專題去總結,現在開始JS之旅的第一部分:JS基礎知識深入總結。下圖是我這篇的大綱。
話在前面:我一直都認為,在網際網路學習的大環境下,網路學習資源很多,但這些部落格等僅僅是一個嚮導,一個跳板,真正學習知識還是需要線下,需要書籍。 另外,如有錯誤,請留言或私信。一起成長,謝謝。
1.1 資料型別的分類和判斷
1.1.1 資料型別的分類
- 基本(值)型別 [primitive values]
基本型別 | 型別的值 | 檢測方法 |
---|---|---|
Number | 可以任意數值 | 用typeof檢測結果為number |
String | 可以任意字串 | 用typeof檢測結果為string |
Boolean | 只有true/false | 用typeof檢測結果為boolean |
undefined | 只有undefined | 用typeof檢測資料型別和‘===’(全等符號) |
null | 只有null | ‘===’(全等符號) |
Symbol | 通過Symbol()得到,值可任意 | 用typeof可檢測結果為symbol |
- 物件(引用)型別 [reference values]
物件型別 | 描述 | 檢測方法 |
---|---|---|
Object | 可以任意物件 | 可以用typeof/instanceof檢測資料型別 |
Array | 一種特別的物件(有數值下標,而且內部資料是有序的。一般的物件內部的資料是無序的,比如你一個物件中有name和age,他們是無序的。) | instanceof |
Function | 一種特別的物件(可以去執行的物件,內部包含可執行的程式碼。一個普通的物件可以執行嗎?不能。)另外,物件是儲存一些資料的,當然函式也是儲存一些程式碼資料。 | typeof |
Date | 時間物件 | instanceof |
RegExp | 正則物件 | instanceof |
1.1.2 基本/物件資料型別特點比較
基本資料型別 | 物件型別 |
---|---|
基本型別的值是不可變的 | 引用型別的值是可變的 |
基本型別的比較是它們的值的比較 | 引用型別的比較是引用(指標指向)的比較 |
基本型別的變數是存放在棧記憶體(Stack)裡的 | 引用型別的值是儲存在堆記憶體(Heap)中的物件(Object) |
1.1.3 資料型別的判斷
-
typeof 注1:用
typeof
判斷返回資料型別的字串(小寫)表達。比如:typeof ‘hello’
結果是string
。 注2:用typeof
來測試有以下七種輸出結果:number
string
boolean
object
function
symol
undefined
。 因此typeof不能去判斷出null
與object
,因為用typeof
去判斷null
會輸出object
。 注3:所有的任何物件,用typeof
測試資料型別都是object
。因此,typeof
不能去判斷出object
與array
。 -
===(全等符號) 注1:只可以判斷undefined 和 null 因為這兩種基本型別的值是唯一的,即可用全等符比較。
-
instanceof 注1:
A instanceof B
翻譯就是B的例項物件是A 嗎? 判斷物件的具體型別(到底是物件型別中的Object
Array
Function
Date
RegExp
的哪一個具體的型別),返回一個Boolean值。 -
借調法:
Object.prototype.toString.call()
注1:這種方法只可以檢測出內建型別(引擎定義好的,自定義的不行),這種方法是相對而言更加安全。Object
Date
String
Number
RegExp
Boolean
Array
Math
Window
等這些內建型別。
以上說明都有案例在面試題裡
1.1.3 四個常見問題
- 問題1:undefined與報錯(not defined)的區別?
物件.屬性:屬性不存在則返回undefined
訪問變數:變數不存在則報錯,xx is not defined
從這個點再去看一個簡單的例子:var obj={ name:'lvya' }; console.log(obj.age); //undefined console.log(age); //報錯,age is not defined 複製程式碼
根據上面這個例子,問個問題。請問訪問function Person(name,age,price) { this.name = name this.age = age this.price=price setName=function (name) { this.name=name; } } var p1 = new Person('LV',18,'10w') console.log(p1.price); // 10w 複製程式碼
p1.price
先找啥?後找啥?通過啥來找?(問題問的不好,直接看答案吧) An:p1.price
先找p1
後找price
。 p1是一個全域性變數哦,這個全域性變數本身存在棧記憶體中,它的值是一個地址值,指向new Person
出來的物件。怎麼找呢?先找p1是沿著作用域找的,後找price是沿著原型鏈找的。這就是聯絡,從另外一個方面細看問題。可能這樣看問題,你就可以把原型鏈和作用域可以聯絡起來思考其他問題。
串聯知識點:請你講講什麼是原型鏈和作用域鏈? 我們從a.b這個簡單的表示式就可以看出原型鏈和作用域鏈。(a正如上例的p1)第一步先找a!a是一個變數,通過作用域鏈去查詢,一層一層往外找,一直找到最外層的window,還沒找到那就會報錯,
a is not defined
。 找到a這個變數,它的值有兩種情況:基本資料型別和物件型別。 如果是基本資料型別(除了undefined和null)會使用包裝類,生成屬性。如果是undefined和null就會報錯,顯示不能讀一個undefined或null的屬性。 如果是物件型別,這就是物件.屬性的方式,開始在物件自身查詢,找不到沿著原型鏈去找。原型鏈也找不到的時候,那麼就會輸出undefined
。
-
問題2:undefined與null的區別?
undefined
代表定義未賦值nulll
定義並賦值了, 只是值為null
var a; console.log(a); // undefined a = null; console.log(a); // null 複製程式碼
使用
Object.prototype.toString.call()
形式可以具體列印型別來區別undefined和null。 如果值是undefined
,返回“[object Undefined]”。 如果這個值為null
,則返回“[object Null]”。 -
問題3:什麼時候給變數賦值為null 呢? 初始賦值, 表明這個變數我將要去賦值為物件 結束前, 這個物件不再使用時,讓物件成為垃圾物件(被垃圾回收器回收)
//起始 var b = null // 初始賦值為null, 表明變數b將要賦值為物件型別 //確定賦值為物件 b = ['lvya', 12] //結束,當這個變數用不到時 b = null // 讓b指向的物件成為垃圾物件(被垃圾回收器回收) // b = 2 //當然讓b=2也可以,但不常使用 複製程式碼
-
問題4:變數型別與資料型別一樣嗎? 資料的型別:包含基本資料型別和物件型別 變數的型別(實則是變數記憶體值的型別) JS弱型別語言,變數本身是無型別的。包含
基本型別
: 儲存的就是基本型別的資料(比如:數字1,字串‘hello lvya’,布林值false等)和引用型別
: 儲存的是地址值,這個地址值去指向某個物件。
1.1.4 一張圖看懂JavaScript各型別的關係
1.1.5 談談valueOf( ) 與 toString( )
toString()
和valueOf()
都是在Object.prototype
裡面定義.
-
toString()
表示的含義是把這個物件表示成字串形式, 並且返回這個字串形式. 首先,在Object.prototype
中它對toString()方法的預設實現是"[object Object]"。 驗證一下:var p={}; console.log(p.toString()); //[object Object] 去Object.prototype的去找(輸出他的預設實現) function Person(){ } var p1=new Person(); console.log(p1.toString()); //[object Object] 去Object.prototype的去找(輸出他的預設實現) 複製程式碼
再看一下可以在自己的物件或者原型上對 toString() 進行覆寫(重寫, override)。這時訪問這個物件的toString()方法時,就會沿著原型鏈上查詢,剛好在自身物件上就找到了toString(),這個時候就不再去找原型鏈上的頂端
Object.prototype
的預設的toString()啦,便實現了物件的toString()的重寫。 驗證一下:var p = { toString: function (){ return "100"; } }; //100 這個時候就會在首先在P物件上找toString()方法,這個時候就是對toString方法的重寫 console.log(p.toString()); 複製程式碼
再舉一個重寫的栗子:
var date = new Date(); console.log(date.toString()); //Fri Jan 18 2019 21:13:44 GMT+0800 (中國標準時間) /*從輸出結果可知,Date這個建構函式的原型其實是有toString()方法的, 說明JS引擎已經在Date原型物件中重寫了toString()方法, 故不會在Object.prototype中找*/ console.log(Date.prototype); //發現確實有toString()方法 var n = new Number(1); console.log(n.toString()); //1(字串) /* 同理:這就是說明他們在js引擎內建的包裝物件,說白了,就是內部已經給Number物件上重寫了 toString()方法。這個方法剛好就是將數字轉為字串*/ 複製程式碼
-
valueOf()
應該返回這個物件表示的基本型別的值!在Object.prototype.valueOf
中找到, 預設返回的是this。當需要在物件上重寫valueOf()
時,應該是返回一個基本資料型別的值。 先看一個預設返回的值的情況。(也就是說它是去這個物件的原型鏈的頂端Object.prototype.valueOf
找的valueOf
方法 )function Person(){ } var p1 = new Person(); console.log(p1.valueOf() == p1); //true 複製程式碼
對返回結果的說明:這個時候
p1.valueOf
是在Object.prototype.valueOf
找到的,返回值預設this。此時this就是p1的這個物件。故結果返回true
。 現在看一下重寫valueOf後的情況var p = { toString: function (){ return "100"; }, valueOf : function (){ return 1; } }; console.log(p.toString()); //100(字串) //還來不及去Object.prototype.valueOf 其本身就有了toString方法 故當然讀本身物件的toString()方法 console.log(p.valueOf()); //1(number資料型別) //同理,沒去Object.prototype.valueOf找 而是找其本身的valueOf方法 複製程式碼
我們再來驗證JS引擎對那些內建物件有去重寫
toString()
和valueOf()
呢?var n = new Number(100); console.log(n.valueOf()); //100 (number型別) var s = new String("abc"); console.log(s.valueOf()); //abc (string型別) var regExp = /abc/gi; console.log(regExp.valueOf() === regExp); //true //說明這個時候正則物件上沒有valueOf,是在Object.prototype.valueOf找的,返回this,this指的就是regExp正則物件。 複製程式碼
結論:在JS中, 只有基本型別中那幾個包裝型別進行了重寫, 返回的是具體的基本型別的值, 其他的型別都沒有重寫,是去物件原型鏈的頂層
Object.prototype.valueOf
去找的。
1.1.6 資料型別間的比較
瞭解完valueOf()
和toSting()
方法後,其實他們就是物件與基本資料型別的比較的基礎。我們資料型別,分為基本資料型別和物件型別兩種,故在資料型別比較中,只會有三種情況:
- 基本資料型別間的比較
- 物件型別間的比較
- 基本資料型別與物件型別間的比較
基本資料型別間的比較
規則:如果型別相同,則直接比較; 如果型別不同, 都去轉成
number
型別再去比較 三個特殊點:1.undefined
==null
2.0
和undefined
,0
和null
都不等 3. 如果有兩個NaN
參與比較,則總是不等的。
總結:都是基本資料型別,但當型別不同時,轉為number型別的規律如下:
基本型別中非number型別 | 轉為number型別 |
---|---|
undefined ‘12a’ ‘abc’ ‘\’ |
Nan |
'' ' ' '\t' '0' null false |
0 |
true ‘1’ |
1 |
‘12’ |
12 |
我們來看看ECMA官方文件對轉number型別的說明:
另外 再補充一點,在JS世界裡,只有五種轉Boolean型別是false
的:0
Nan
undefined
null
""
false
。其他的轉Boolean值都是true
。
我們再來看看ECMA官方文件對轉Boolean型別的說明:
所以,從這裡我們就可以發現其實原文的ECMA官方文件就是很棒的學習資料,已經幫你整理的很完備了。多去翻翻這些官方文件的資料很有幫助。
例子1:屬於基本型別間的比較,而且都是基本型別中的number型別,相同型別直接比較。
var a=1;
var b=1;
console.log(a == b); //true
console.log("0" == ""); //false
//都是相同的string型別,不用轉,直接用字串比較
複製程式碼
例子2:屬於基本型別間的比較,但是其具體的型別不同,需要轉為number
型別再去比較。
console.log(true == "true"); //false 相應轉為number型別去比較:1與Nan比較
console.log(0 == "0"); //true 相應轉為number型別去比較:0與0比較
console.log(0 == ""); //true 相應轉為number型別去比較:0與0比較
console.log(undefined == null); //true Nan與0比較??特殊
複製程式碼
例子3:屬於三大特殊點
console.log(undefined == null); //true
console.log(undefined == 0); //false
console.log(null == 0); //false
console.log(Nan == Nan); //false
複製程式碼
物件型別間的比較
物件間的比較中
===
(嚴格相等:值和型別都相等) 和==
完全一樣。 規則:其實比較是不是同一個物件,比的就是他們的地址值是否一樣。
例子1:物件型別間的比較
console.log({} === {}); //false 地址值不同
console.log(new Number(1) == new Number(1)); //false 地址值不同
複製程式碼
基本型別與物件型別間的比較
重點:這就是為啥之前引入
valueOf
和toString()
的道理。 規則:把物件轉成基本型別的資料之後再比 ?如何把物件轉換成基本型別:1. 先呼叫這個物件(注意是物件)的valueOf()
方法, 如果這個方法返回的是一個基本型別的值, 則用這個基本型別去參與比較。 2. 如果valueOf()
返回的不是基本型別, 則去呼叫toString()
然後用返回的字串去參與比較。這個時候就是字串與那個基本型別的比較,問題從而轉為了基本型別間的比較。
例子1:
var p = {};
console.log(p.valueOf()); //{}
console.log(p == "[object Object]"); //true
複製程式碼
解釋:首先明確是物件與基本型別中的字串比較;按照規則,先把物件呼叫其valueOf()
,根據上節知識可知,返回的是this,也就是當前物件{}。不是基本資料型別,故再呼叫其toString()
,返回"[object Object]"
,從而進行基本資料型別間的比較,根據規則,型別相同都是字串,直接比較,故相等。
例子2:
var p1 = {
valueOf : function (){
return 'abc';
},
toString : function (){
return {};
}
}
console.log(p1 == "abc"); //true
複製程式碼
解釋:首先明確是物件與基本型別中的字串比較;按照規則,先把物件呼叫其valueOf()
,根據上節知識可知,p有重寫 valueOf
,故直接輸出字串'abc'
,它屬於基本資料型別,故不再呼叫其toString()
。進而進行基本資料型別間的比較,根據規則,型別相同都是字串'abc'
,直接比較,故相等。
1.1.7 案例習題與面試題
案例1: 基本資料型別的判斷
typeof
返回資料型別的字串(小寫)表達
var a;
console.log(a, typeof a, typeof a === 'undefined', a === undefined) // undefined 'undefined' true true
console.log(undefined === 'undefined'); //false(轉為number實則是Nan與0的比較)
a = 4;
console.log(typeof a === 'number'); //true
a = 'lvya';
console.log(typeof a === 'string'); //true
a = true;
console.log(typeof a === 'boolean'); //true
a = null;
console.log(typeof a, a === null); // 'object' true
複製程式碼
案例2: 物件型別的判斷
var b1 = {
b2: [1, 'abc', console.log],
b3: function () {
console.log('b3');
return function () {
return 'ya Lv'
}
}
};
console.log(b1 instanceof Object, b1 instanceof Array); // true false
console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) ;// true true
console.log(b1.b3 instanceof Function, b1.b3 instanceof Object); // true true
console.log(typeof b1.b2); // 'object'
console.log(typeof b1.b3 === 'function');// true
console.log(typeof b1.b2[2] === 'function'); // true
b1.b2[2](4); //4
console.log(b1.b3()()); //ya Lv
複製程式碼
instanceof
一般測物件型別,那它去測基本資料型別會出現怎樣的奇妙火花呢?一起來驗證一下。instanceOf
內部的實現原理可以直接看3.2.3節。
//1並不是Number型別的例項
console.log(1 instanceof Number); //false
//new Number(1)的確是Number型別的例項
console.log(new Number(1) instanceof Number); //true
複製程式碼
面試3: 考察typeOf
檢測資料型別
用
typeof
來測試有以下七種輸出結果:'number'
'string'
'boolean'
'object'
'function'
'symol'
'undefined'
。注意都是字串表達方式。
console.log(typeof "ab"); // string
console.log(String("ab")); //'ab' 可以知道String("ab")就是var s='ab'的含義
console.log(typeof String("ab")); // string
console.log(typeof new String("ab")); // object
console.log(typeof /a/gi); // object
console.log(typeof [0,'abc']); // object
console.log(typeof function (){}); //function
var f = new Function("console.log('abc')");
f(); //'abc' 可以知道f就是一個函式
console.log(typeof f); //function
console.log(typeof new Function("var a = 10")); //function
複製程式碼
面試4: 考察+
的運用
JS加號有兩種用法: Case 1:數學上的加法(只要沒有字串參與運算就一定是數學上的數字): Case 2:字串連線符(只要有一個是字串,那就是字串連結)
console.log(1 + "2" + "2"); // 122
console.log(1 + +"2" + "2"); // 32 (這裡+'2'前面的加號是強轉為number的意思)
console.log(1 + -"1" + "2"); // 02
console.log(+"1" + "1" + "2"); // 112
console.log( "A" - "B" + "2"); // NaN2
console.log( "A" - "B" + 2); // NaN
複製程式碼
面試5: 考察valueOf
和toString
console.log([] == ![]); //true
複製程式碼
說明:首先左邊是[]
,右邊是![]
這個是一個整體,由1.1.6節知識可知,世界上只有五種轉Boolean值得是false
,其他都是true
。故右邊這個![]
整體結果是false
。綜上,明確這是物件與基本型別(布林值)的比較。
然後,就是將物件先呼叫valueOf
後呼叫toString
的規則去判斷,由1.1.6節可知,左邊是物件,首先用valueOf
返回的是一個陣列物件(注意如果是{}
。valueOf()
就是返回this
,此時this
是{}
!)
然後再呼叫toString
返回一個空的字串,因為陣列轉字串,就是去掉左右“中括號”,把值和逗號轉為字串,看一下驗證:
console.log([].valueOf()); //[]
console.log([].valueOf().toString()); //空的字串
複製程式碼
故左邊是一個空的字串。右邊是false
。又轉為基本資料間的比較,兩個不同型別,則轉為number
型別去比較。
空字串轉number
為0,false
轉number
為0。故0==0
結果就是true
。
面試6: &&
||
的短路現象
&&
||
在js中一個特別靈活的用法。如果第一個能最終決定結果的,那麼結果就是第一個值,否則就是第二個。這個在實際專案中使用也很常見。 與和或的優先順序:"與" 高於 "或",也就是&&
優先順序大於||
console.log(1 && 2 || 0); // 2
console.log((0 || 2 && 1)); //1 (注意,這裡是先計算2 && 1,因為&&優先順序高於||)
console.log(3 || 2 && 1); // 1 (注意,這裡是先計算2 && 1,因為&&優先順序高於||)
console.log(0 && 2 || 1); // 1
複製程式碼
面試7: 型別轉換綜合題
-
+當做數字相加,因為兩邊都沒字串,故都轉number
var bar = true; console.log(bar + 0); // 1 複製程式碼
var bar = true; console.log(bar + true); // 2 複製程式碼
var bar = true; console.log(bar + false); // 1 複製程式碼
var bar = true; console.log(bar + undefined); // Nan 複製程式碼
var bar = true; console.log(bar + null); // 1 複製程式碼
console.log(undefined + null); // Nan (Nan與任何運算結果都是Nan) 複製程式碼
-
+當做字串連線,因為有一個為字串
var bar = true; console.log(bar + "xyz"); // truexyz 複製程式碼
-
隱含的型別轉換
console.log([1, 2] + {}); //1,2[object Object] 複製程式碼
Array.prototype.valueOf = function () { return this[0]; }; console.log([1, 2] + [2]); //3 /**重寫了Array的valueOf方法,其重寫後返回的是this[0], 因為在這是number型別1,故直接用。*/ 複製程式碼
console.log([{}, 2] + [2]); // [object Object],22 /**重寫了Array的valueOf方法,其重寫後返回的是this[0], 因為在這是一個物件{},故重新在對這個陣列物件([{},2])呼叫toString()返回‘[object Object],2’。 這裡要注意當呼叫toString是整個物件,而非重寫valueOf後返回來的物件。 +右邊的[2]是呼叫了valueOf之後返回的number型別2,所以直接用, 因為左邊是一個字串,所以加號代表字串拼接。返回最終結果[object Object],22 */ 複製程式碼
1.2 資料,變數, 記憶體的理解
1.2.1 什麼是資料?
-
儲存在記憶體中特定資訊的"東東",本質上是0101...的二進位制
-
資料的特點:可傳遞, 可運算
var a = 3; var b = a; 複製程式碼
這裡體現的就是資料的傳遞性。變數a是基本資料型別,儲存的值是基本資料型別number值為3。在棧記憶體中儲存。這兩個語句傳遞的是變數a嗎?不是。傳遞的是資料3。實際上,是拿到變數a的內容數字3拷貝一份到b的記憶體空間中。
注意:不管在棧記憶體空間儲存的基本資料型別還是在堆記憶體中儲存的物件型別,這些記憶體都有地址值。只是要不要用這個地址值的問題。物件的地址值一般會用到。所以很多人會誤以為只有物件才有地址值,這是錯誤的理解。
- 在記憶體中的所有操作的目標是資料
- 算術運算(加減乘除)
- 邏輯運算(與或非)
- 賦值(=)
- 執行函式(例如執行fn(),此時()就是可以看做是一種運算元據的方式,去執行程式碼塊)
1.2.2 什麼是變數?
- 在程式執行過程中它的值是允許改變的量,由變數名和變數值組成
- 一個變數對應一塊小記憶體,它的值儲存在這個記憶體中。變數名用來查詢對應的記憶體, 變數值就是記憶體中儲存的資料。通過變數名先去找到對應的記憶體,然後再去操作變數值。
1.2.3 什麼是記憶體?
-
記憶體條通電後產生的可儲存資料的空間(臨時的),它是臨時的,但處理資料快
-
硬碟的資料是永久的,但其處理資料慢
-
記憶體產生和死亡: 記憶體條(電路版) -> 通電 -> 產生記憶體空間 -> 儲存資料 -> 處理資料 -> 斷電 -> 記憶體空間和資料都消失
-
記憶體空間的分類:
-
棧空間: 全域性變數和區域性變數【空間比較小】
-
堆空間: 物件 (指的是物件(函式也是物件)本身在堆空間裡,其本身在堆記憶體中。但函式名在棧空間裡。)【空間比較大】
//obj這個變數在棧空間裡 name是在堆空間裡 function fn () { var obj = {name: 'lvya'} } 複製程式碼
-
-
一塊小的記憶體包含2個方面的資料
-
內部儲存的資料(內容資料)
-
地址值資料(只有一種情況讀的是地址值資料,那就是將一個物件給一個變數時)
var obj = {name: 'lvya'} ; var a = obj ; console.log(obj.name) ; 複製程式碼
執行
var obj = {name: 'Tom'}
是將右邊的這個物件的地址值給變數obj,變數obj這個記憶體裡面儲存的就是這個物件的地址值。 而var a = obj
右邊不是一個物件,是一個變數(引用型別的變數),把obj的內容拷貝給a,而剛好obj的儲存的內容是一個物件的地址值。 執行console.log(obj.name)
讀的是obj.name的內容值。 總結:什麼時候讀的是地址值?只有把一個物件賦值給一個變數時才會讀取這個物件在記憶體塊中的地址值資料。上述三條語句只有var obj = {name: 'Tom'}
才是屬於讀地址值的情況。
-
1.2.4 記憶體,資料, 變數三者之間的關係
- 記憶體是容器, 用來儲存不同資料
- 變數是記憶體的標識, 通過變數我們可以操作(讀/寫)記憶體中的資料
1.2.5 一些相關問題
問題一:var a = xxx, a記憶體中到底儲存的是什麼?
需要分類討論:
-
當xxx是基本資料, 儲存的就是這個資料
var a = 3; //3是基本資料型別,變數a儲存的就是3. 複製程式碼
-
當xxx是物件, 儲存的是物件的地址值
a = function () { } //函式是物件,那麼a儲存的就是這個函式物件的地址值。 複製程式碼
-
當xxx是一個變數, 儲存的xxx的記憶體內容(這個內容可能是基本資料, 也可能是地址值)
var b = 'abc' a = b //b是一個變數,而b本身記憶體中的內容是一個基本資料型別。 //所以,a也是儲存這個基本資料型別'abc' 複製程式碼
b = {} a = b //b是一個變數,而b本身記憶體中的內容是一個物件的地址值。 //所以,a也是儲存這個物件的地址值'0x123' 複製程式碼
問題二:關於引用變數賦值問題?
-
2個引用變數指向同一個物件, 通過一個變數修改物件內部資料, 另一個變數看到的是修改之後的資料
var obj1 = {name: 'Tom'} var obj2 = obj1 obj2.name = 'Git' console.log(obj1.name) // 'Git' function fn (obj) { obj.name = 'A' } fn(obj1) console.log(obj2.name) //A 複製程式碼
執行
var obj2 = obj1
obj1
是一個變數,而非物件。故把obj1的內容拷貝給obj2,只是剛好這個內容是一個物件的地址值。這個時候,obj1
obj2
這兩個引用變數指向同一個物件{name: 'Tom'}
。 通過其中一個變數obj2
修改物件內部的資料。obj2.name = 'Git'
那麼這個時候,另外一個物件看到的是修改後的結果。當然,後面的對fn(obj1)
,也是一樣的操作,涉及到實參和形參的賦值。 -
2個引用變數指向同一個物件, 讓其中一個引用變數指向另一個物件, 另一引用變數依然指向前一個物件。
var a = {age: 12}; var b = a; a = {name: 'BOB', age: 13}; b.age = 14; console.log(a.name, a.age,b.age);// 'BOB' 13 14 function fn2 (obj) { obj = {age: 100} console.log(obj.age); //100 } fn2(a); //函式執行完後會釋放其區域性變數 console.log(a.age,b.age) //13 14 console.log(obj.age); //報錯 obj is not defined 複製程式碼
一開始兩個引用變數
a
b
都指向同一個物件,而後執行a = {name: 'BOB', age: 13};
語句,就是讓a
指向另一個物件{name: 'BOB', age: 13}
,a
中的內容的地址值變化了。而b
還是指向之前a
的那個物件{age: 12}
。 這個例子要好好理解,看圖解:未執行fn2函式之前和執行fn2函式後
問題三:在JS呼叫函式時傳遞變數引數時,是值傳遞還是引用傳遞?
-
理解1: 都是值(基本型別的值/物件的地址值)傳遞
-
理解2: 可能是值傳遞, 也可能是引用傳遞(這個時候引用傳遞理解為物件的地址值)
var a = 3 function fn (a) { a = a +1 } fn(a) console.log(a) //3 複製程式碼
fn(a)
中的a
是一個實參。function fn (a)
中的a
是一個形參。var a = 3
中的a
是一個全域性變數,其記憶體中儲存的內容是基本資料型別3。實參與形參的傳遞是把3傳遞(拷貝)給形參中的a的記憶體中的內容。傳遞的不是a!而是3。然後,執行完之後,函式裡面的區域性變數a
被釋放,當輸出a
值時,肯定去讀取全域性變數的a
。傳遞的是值3。function fn2 (obj) { console.log(obj.name) //'Tom' } var obj = {name: 'Tom'} fn2(obj) 複製程式碼
fn2(obj)
中的實參obj
(引用變數)把其內容(剛好是地址值)傳遞給形參中的內容。而不是指把{name: 'Tom'}
整個物件賦值給形參obj
。是把地址值拷貝給形參obj
。也就是實參obj
形參obj
這兩個引用變數的內容一樣(地址值一樣)。傳遞的是地址值。
問題四:JS引擎如何管理記憶體?
-
記憶體生命週期 分配小記憶體空間, 得到它的使用權 儲存資料, 可以反覆進行操作 釋放小記憶體空間
-
釋放記憶體 區域性變數:函式執行完自動釋放(全域性變數不會釋放記憶體空間) 物件:成為垃圾物件==>垃圾回收器回收(會有短暫間隔)
var a = 3 var obj = {} //這個時候有三塊小的記憶體空間:第一塊 var a = 3 第二塊 var obj 第三塊 {} 在堆記憶體中 obj = undefined //不管obj = null/undefined 這個時候記憶體空間還有兩塊。沒有用到的{}會有垃圾回收器回收。 function fn () { var b = {} //b區域性變數 整個區域性變數的生命週期是在函式開始執行到結束。 } fn() // 區域性變數b是自動釋放, b所指向的物件是在後面的某個時刻由垃圾回收器回收 複製程式碼
1.3 物件的理解和使用
這一節主要是對物件的基本理解和使用作出闡述,一些基本的問題筆者會簡單地在Part 1這部分羅列出來。具體的深入問題在Part 2中深入探討。那麼在瞭解物件的概念時,思想很重要,那就是物件是如何產生的?物件內部有啥需要關注?至於物件如何產出,有new出來,字面量定義出來的,函式return出來的等等情況。至於內部有啥呢,主要關注就是屬性和方法這兩個東西。
什麼是物件?
- 變數可以存資料,物件也可以存資料,那麼他與變數功能就會有差異。
- 物件是多個資料(屬性)的集合
- 也可以說物件是用來儲存多個資料(屬性)的容器
- 一個物件就是描述我們生活中的一個事物。
為什麼要用物件?
統一管理多個資料。如果不這麼做,那麼就要引入很多的變數。
比如我現在要建立一個物件Person
,裡面有name
age
gender
等等,我可以用一個物件去建立資料容器,就不需要單獨設定很多變數了。方便管理。
物件的分類?
- 內建物件---在任何ES實現中都可以使用(不需new,直接引用)比如
Math
/String
/Function
/Number
/Data
- 宿主物件---由JS執行環境提供物件(瀏覽器提供)所有的BOM和DOM物件都是宿主物件。比如
console.log()
document.write()
- 自定義物件---由開發人員建立
物件的組成?
- 屬性: 屬性名(字串)和屬性值(任意型別)組成-----描述物件的狀態資料
- 方法: 一種特別的屬性(屬性值是函式)-----描述物件的行為資料
- 它們之間的聯絡就是方法是特殊的屬性。
在瞭解完物件之後,我們知道每個物件會去封裝一些資料,用這個物件去對映某個事物。那麼這些資料就是由屬性來組成的,現在我們看看屬性的一些相關知識。
屬性組成?
-
屬性名 : 字串(標識),本質上是字串。本質上屬性名是加引號的,也就是字串。但一般實際操作都不加。
-
屬性值 : 任意型別(所以會有方法是特別的屬性這一說法。)
屬性名本質上是字串型別,見下例:
var obj={ 'name':'豬八戒'; 'gender':'男'; } 複製程式碼
上例子一般我們不會特意這樣去將屬性名打上雙引號,我們一般習慣這樣寫:
var obj={ name:'豬八戒'; gender:'男'; } 複製程式碼
再看一道物件屬性名知識點的面試題:
var a = {}, b = {key: 'b'}, c = {key: 'c'}; a[b] = 123; // a["[object Object]"] = 123 a[c] = 456; // a["[object Object]"] =456 console.log(a[b]); //輸出456 求a["[object Object]"]=? 複製程式碼
上例解釋:屬性名本質上是字串。ES6之前物件的屬性名只能是字串, 不能是其他型別的資料! 如果你傳入的是其他型別的資料作為屬性名, 則會把其他型別的資料轉換成字串,再做屬性名。若是物件,那麼就呼叫
toString()
,ES6 屬性名可以是Symbol
型別。 再看一個例子:var a = { "0" : "A", "1" : "B", length : 2 }; for(var i = 0; i < a.length; i++){//a是物件,a.length是讀取物件的屬性,為2. console.log(a[i]); } //會輸出A B 複製程式碼
再看一個例子:
var a = {}; a[[10,20]] = 2000; //首先把握好a是物件,a[]就是使用物件讀其屬性的語法,而不是陣列。把a[]中[10,20]本質上是字串,所以要轉啊。 //[10,20]轉字串就是物件轉字串,呼叫toString(),變成“10,20”。這個轉的字串就是屬性名。 console.log(a); // 輸出 {10,20: 2000} 複製程式碼
屬性的分類?
- 一般 : 屬性值不是function ,描述物件的狀態
- 方法 : 屬性值為function的屬性 ,描述物件的行為
陣列和函式是特別的物件?
- 陣列: 屬性名是0,1,2,3之類的索引(有序)
- 函式: 可以執行的
如何訪問物件內部的資料 ?
.屬性名
: 編碼簡單, 有時不能用。['屬性名']
:編碼麻煩, 能通用。
var p = {
name: 'Tom',
age: 12,
setName: function (name) {
this.name = name
},
setAge: function (age) {
this.age = age
}
};
p.setName('Bob') //用.屬性名的方式
p['setAge'](23) //用['屬性名']語法
console.log(p.name, p['age']) //Bob 23
複製程式碼
什麼時候必須使用['屬性名']
的方式?
- 屬性名包含特殊字元: - 或 空格
- 屬性名不確定時。
var p = {};
//1. 給p物件新增一個屬性: content type: text/json
// p.content-type = 'text/json' //不能用
p['content-type'] = 'text/json'
console.log(p['content-type'])
//2. 屬性名不確定,用變數去儲存這個值。
var propName = 'myAge'
var value = 18
// p.propName = value //不能用
p[propName] = value //propName代表著的就是一個變數
console.log(p[propName]) //18
複製程式碼
函式物件(Function Object)是什麼呢?
- 函式作為物件使用的時候,這個函式就是函式物件。
1.4 函式的理解和使用
其實在JavaScript中筆者認為最複雜的資料型別,不是物件,而是函式。為什麼函式是最複雜的資料型別呢?因為函式可以是物件,它本身就會有物件的複雜度。函式又可以執行,它有很多的執行呼叫的方式(這也決定了函式中this
是誰的問題),所以他又有函式執行的複雜度。這一小節我們就簡單來說說函式的基本知識。在Part3會去更深入去介紹JS中的函式。
什麼是函式?
- 定義:用來實現特定功能的, n條語句的封裝體,在需要的時候執行此功能函式。
- 注:只有函式型別的資料是可以執行的, 其它的都不可以
為什麼要用函式?
-
提高複用性(封裝程式碼)
-
便於閱讀交流
function showInfo (age) { if(age<18) { console.log('未成年, 再等等!') } else if(age>60) { console.log('算了吧!') } else { console.log('剛好!') } } //如果不用函式做,也可以,但要把中間的程式碼書寫很多遍。 //而函式就是抽象出共同的東西,把這些執行過程封裝起來,給大家一起用。 showInfo(17) //未成年, 再等等! showInfo(20) //剛好! showInfo(65) //算了吧! 複製程式碼
如何定義函式 ?
-
函式宣告
-
表示式
-
建立函式物件
var fun = new Function( ) ;
一般不使用function fn1 () { //函式宣告 console.log('fn1()') } var fn2 = function () { //表示式 console.log('fn2()') } fn1(); fn2(); 複製程式碼
如何呼叫(執行)函式?
-
test()
: 直接呼叫 -
obj.test()
: 通過物件去呼叫 -
new test()
: new呼叫 -
test.call/apply(obj)
: 臨時讓test成為obj物件的方法進行呼叫var obj = {} //一個物件 function test2 () { //一個函式 this.xxx = 'lvya' } // obj.test2() 不能直接, 根本obj物件中就沒有這樣的函式(方法) test2.call(obj) // 相當於obj.test2() , 可以讓一個函式成為指定任意物件的方法進行呼叫 console.log(obj.xxx) //lvya //這個借調是JS有的,其他語言做不到。借調就是假設一個物件中沒有一個方法, //那麼就可以讓這個方法成為想要呼叫這個方法的物件去使用的方式。 //也就是一個函式可以成為指定任意物件的方法進行呼叫 。 複製程式碼
函式也是物件
- instanceof Object===true
- 函式有屬性:
prototype
- 函式有方法:
call()
/apply()
- 可以新增新的屬性/方法
函式的3種不同角色
- 一般函式 : 直接呼叫
- 建構函式 : 通過new呼叫
- 物件 : 通過
物件.
呼叫內部的屬性/方法
this是什麼?
- 任何函式本質上都是通過某個物件來呼叫的,如果沒有直接指定就是window。
- 所有函式內部都有一個變數this,它的值是呼叫函式的當前物件
- <具體this總結見Part2部分>
如何確定this的值?
-
test()
: window -
p.test()
: p -
new test()
: 新建立的物件 -
p.call(obj)
: obj -
回撥函式: 看背後是通過誰來呼叫的: window/其它
<script type="text/javascript"> function Person(color) { console.log(this) this.color = color; this.getColor = function () { console.log(this) return this.color; }; this.setColor = function (color) { console.log(this) this.color = color; }; } Person("red"); //this是誰? window var p = new Person("yello"); //this是誰? p(Person) p.getColor(); //this是誰? p (Person) var obj = {}; p.setColor.call(obj, "black"); //this是誰? obj (Object) var test = p.setColor; test(); //this是誰? window function fun1() { function fun2() { console.log(this); } fun2(); } fun1(); //this是誰? window </script> 複製程式碼
匿名函式自呼叫:
(function(w, obj){
//實現程式碼
})(window, obj)
複製程式碼
- 專業術語為: IIFE (Immediately Invoked Function Expression) 立即呼叫函式表示式
- 作用
- 隱藏實現 (讓外部的全域性看不到裡面)
- 不會汙染外部(全域性)名稱空間
- 用它來編碼js模組
;(function () { //匿名函式自呼叫
var a = 1
function test () {
console.log(++a)
}
window.$ = function () { // 向外暴露一個全域性函式
return {
test: test
}
}
})()
$().test() //需明白 1. $是一個函式 2. $執行後返回的是一個物件 3.然後物件.方法()執行函式。
複製程式碼
回撥函式的理解
- 什麼函式才是回撥函式?
- 你定義的
- 你沒有呼叫
- 但它最終執行了(在一定條件下或某個時刻)
- 常用的回撥函式
回撥函式型別 | this是指向誰? |
---|---|
DOM事件回撥函式 | 發生事件的DOM元素 |
定時器回撥函式 | Window |
ajax請求回撥函式 | Window |
生命週期回撥函式 | 元件物件 |
函式中的arguments 在呼叫函式時,瀏覽器每次都會傳遞兩個隱含的引數:
- 函式的上下文物件
this
- 封裝實參的物件
arguments
(類陣列物件)。這裡的實參是重點,就是執行函式時實際傳入的引數的集合。
function foo() {
console.log(arguments); //Arguments(3)返回一個帶實引數據的類陣列
console.log(arguments.length); //3 類陣列的長度
console.log(arguments[0]); //ya LV 可以不傳形參,可以訪問到實參
console.log(arguments.callee); // ƒ foo() {...} 返回對應當前正在執行函式的物件
}
foo('ya LV',18,'male');
複製程式碼
arguments妙用1:利用arguments實現方法的過載
-
a.借用arguments.length屬性來實現
function add() { var len = arguments.length, sum = 0; for(;len--;){ sum += arguments[len]; } return sum; } console.log( add(1,2,3) ); //6 console.log( add(1,3) ); //4 console.log( add(1,2,3,5,6,2,7) ); //26 複製程式碼
-
b.借用prototype屬性來實現
function add() { return Array.prototype.reduce.call(arguments, function(n1, n2) { return n1 + n2; }); }; add(1,2,3,6,8); //20 //三個常用的陣列的高階函式:map(對映)filter(過濾)reduce(歸納) //可以參見ES6函式新增特性之箭頭函式進一步優化 複製程式碼
arguments妙用2.利用arguments.callee
實現遞迴
先來看看之前我們是怎麼實現遞迴的,這是一個計算階乘的函式:
function factorial(num) {
if(num<=1) {
return 1;
}else {
return num * factorial(num-1);
}
}
複製程式碼
但是當這個函式變成了一個匿名函式時,我們就可以利用callee
來遞迴這個函式。
function factorial(num) {
if(num<=1) {
return 1; //如果沒有這個判斷,就會記憶體溢位
}else {
return num * arguments.callee(num-1);
}
}
console.log(factorial(5)); //120
複製程式碼
1.5 補充
1.5.1 分號問題
-
js一條語句的後面可以不加分號,類似“可以加分號但是大家都不加” 的語言就有:
Go
,Scala
,Ruby
,Python
,Swift
,Groovy
... -
是否加分號是編碼風格問題, 沒有應該不應該,只有你自己喜歡不喜歡
-
在下面2種情況下不加分號會有問題
小括號開頭的前一條語句
var a = 3 ;(function () { })() //如果不加分號就會這麼錯誤解析: // var a = 3(function () { // })(); 複製程式碼
中方括號開頭的前一條語句
var b = 4 ;[1, 3].forEach(function () { }) // 如果不加分號就會這麼錯誤解析: // var b = 4[3].forEach(function () { // }) 複製程式碼
-
解決辦法: 在行首加分號
-
強有力的例子:
Vue.js
庫。Vue.js
的程式碼全部不帶分號。 -
有一個工具全自動幫你批量新增或者刪除分號:工具地址
1.5.2 位運算子和移位在JS中的操作
像二進位制,八進位制,十進位制,十六進位制這些概念在JavaScript中很少被體現出來,可是筆者覺得這個是碼農的素養,所以我覺得有必要再去搞懂。另外一個就是原碼反碼補碼的概念。比如在計算機硬體電路中有加法器,所有的運算都會轉為加法運算,減法就是用加法來實現。所以才引出原碼反碼補碼的概念去解決這一問題。
那麼筆者現在著重講一下位運算子操作和移位操作。js中位運算子有四種:按位取反(~
)、按位與(&
)、按位或(|
)、按位異或(^
)。移位操作有四種:帶符號向右移動(>>
)、無符號向右移動(>>>
)、帶符號向左移動(<<
)、無符號向左移動(<<<
).
示例1:如何快速判斷一個數是不是奇數?
那麼,取餘是你先想到的,那麼還有其他方法嗎?就是用位運算子去解答。先思考奇數3(二進位制是11),偶數4(二進位制是100),可知偶數的最低位為0,奇數的最低位為1,那麼我們只要通過某種方法得到一個數的二進位制的最低位,判斷它是不是為1,是1那這個數就是奇數。
現在的問題就變成了,怎麼得到一個數的二進位制最低位呢?那就是用按位與1(& 1
)去做。假設一個數的二進位制為1111 1111 那麼只要按位與1(1的二進位制為0000 0001)是不是前面一排“與0”都變成0了,只剩最低位了,這樣目標數與1的按位與運算的結果就是等價於目標數的二進位制最低位。
var num = 57 ;
if(num & 1){
console.log(num + "是奇數"); //57是奇數
}else{
console.log(num + "是偶數");
}
複製程式碼
示例2:怎麼交換兩個number
型別的變數值?
新增一個變數來儲存這種方式是你先想到的,那麼另外一種就是通過按位異或操作去交換變數。
異或就是不同的為true(1),相同的為false(0)。
10^10=0 因為1010 ^ 1010 = 0000
11^0=11 因為1011 ^ 0000=1011
所以得到兩個結論:
第一,兩個相同的number數異或為0;第二,任何number數與0異或是其本身。
var a = 10;
var b = 20;
a = a ^ b; //a=10 ^20
b = a ^ b; //b=10 ^20^20 =10 ^(20^20)=10^0=10
a = a ^ b; //a=10 ^20^ 10 =(10^10)^20=0^20=20
console.log(a, b); //20 10 -交換變數成功-
//但這種方法只適用於number資料型別。
//另外可以用ES6中的陣列解構。
複製程式碼
示例3:如何計算出一個數字某個二進位制位?
在回答這個問題前,我們先總結出一些結論供我們使用。移位都是當做32位來移動的,但我們這裡就簡單操作,用8位來模擬。
先看帶符號向右
移位:
10 >>
1 翻譯題目:10帶符號向右移動一位是幾?
0000 1010 >>
1
0000 0101 這個結果就是移位後的結果。我們可以知道0101就是十進位制的5.
帶符號向右移動就是整體向右移動一位,高位用符號位去補。正數用0補,負數用1補。
我們可以看出結論,帶符號向右移動其實就是往右移動一位,相當於除以2.
現在再來看看帶符號向左移位:
10 <<
2 翻譯題目: 10帶符號向左移動2位是幾?
0000 1010 <<
2
0010 1000 低位用0補。這個 0010 1000 就是數就是40.
我們可以看出結論,帶符號向左移動其實就是往左移動一位,相當於乘以2。移動2位即是乘4。
現在迴歸題目,假設我要知道10(1010)的倒數第三位的0這個進位制位。
首先往右移動兩位變成0010 , 然後進行 ‘&1
’ 操作 , 0010 &
0001 =0000 =0 ,這個0就是10的二進位制位的倒數第三位。所以是通過:(10 >>
2 &
1)的方式得到10的倒數第三位的進位制位。
示例4:如何計算出2的6次方最快演算法?
2B程式猿會用2 * 2 * 2 * 2 * 2 * 2
的方法吧。碼農可能會用for
迴圈去做或者用Math.pow(2,6)
去寫。
但是這些都不是最快的。我們來看看高階工程師會怎麼寫,哈哈。我們剛剛得到過2個結論,其中一個就是帶符號向左移位其實就是往左移動一位,相當於乘以2。 移動2位,就是乘4。"左乘右除"
。那麼現在我是不是可以對 1 移動6位 不就可以了嗎?所以就一行程式碼:1 <<
6 。
由彙編知識我們知道,移位是最底層的計算。可以完全用加法器實現。而Math.pow(2,6)
其實會有很多的彙編指令才可以實現這一條程式碼。但1 <<
6 只需要一條,所以,效能是很好的。
1.5.3 記憶體溢位與記憶體洩露
記憶體溢位:
-
一種程式執行出現的錯誤
-
當程式執行需要的記憶體超過了剩餘的記憶體時, 就出丟擲記憶體溢位的錯誤。
// 1. 記憶體溢位 var obj = {} for (var i = 0; i < 10000; i++) { obj[i] = new Array(10000000) console.log('-----') } //直接崩掉了,需要的記憶體大於目前空閒的記憶體,直接報錯誤:記憶體不足。 //就如一個水杯,水倒滿了就溢位,這就是記憶體溢位。 複製程式碼
記憶體洩露:
-
佔用的記憶體沒有及時釋放,這時程式還是可以正常執行的
-
記憶體洩露積累多了就容易導致記憶體溢位
-
常見的記憶體洩露:
-
意外的全域性變數
// 在意外的全域性變數--在ES5的嚴格模式下就會報錯。 function fn() { a = new Array(10000000) console.log(a) } fn() //a就是意外的全域性變數,一直會佔著記憶體,關鍵它還是指向一個陣列非常大的物件。這塊記憶體就一直佔著。 複製程式碼
-
沒有及時清理的計時器或回撥函式
// 沒有及時清理的計時器或回撥函式 var intervalId = setInterval(function () { //啟動迴圈定時器後不清理 console.log('----') }, 1000) // clearInterval(intervalId) 複製程式碼
-
閉包
// 閉包 function fn1() { var a = 4 function fn2() { console.log(++a) } return fn2 } var f = fn1() f() //f指向的fn2函式物件一直都在,設f指向空物件,進而讓fn2成為垃圾物件,進而去回收閉包。 // f = null 複製程式碼
1.5.3 函式節流與函式防抖
函式節流:讓一段程式在規定的時間內只執行一次
<script type="text/javascript">
window.onload = function () {
// 函式節流: 讓一段程式在規定的時間內只執行一次
let flag = false;
document.getElementsByTagName('body')[0].onscroll = function () {
if(flag){
return;
}
flag = true; //當之前為FALSE的時候進來,我在2s內才會有定時器註冊。
setTimeout(function () {
console.log('滾動過程中,2s只會註冊定時器一次');
flag = false; //一次後,為第二次做準備。只要是TRUE,我就進不來註冊定時器的邏輯
}, 2000)
}
}
</script>
複製程式碼
函式防抖: 讓某一段程式在指定的事件之後觸發
<script type="text/javascript">
//場景:讓滾動完之後2s觸發一次。
window.onload = function () {
// 函式防抖: 讓某一段程式在指定的事件之後觸發
let timeoutId = null;
document.getElementsByTagName('body')[0].onscroll = function () {
timeoutId && clearTimeout(timeoutId); //當第一次來的時候為null,不需要清除定時器。
timeoutId = setTimeout(function () {
console.log('xxx');
}, 2000) //在這個滾動過程中的最後一次才註冊成功了,其他的定時器都註冊完後馬上清除。
}
}
</script>
複製程式碼
此文件為呂涯原創,可任意轉載,但請保留原連結,標明出處。
文章只在CSDN和掘金第一時間釋出:
CSDN主頁:https://blog.csdn.net/LY_code
掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948
若有錯誤,及時提出,一起學習,共同進步。謝謝。 ???