前端 - JS進階
目錄
基礎總結
資料型別
- 基本資料型別:
String
Number
boolean
undefined
null
- 物件 (引用) 型別:
-
Object
:任意物件 -
Function
:特殊的物件,可以執行 -
Array
:特殊的物件,內部資料有序,數值索引
-
判斷資料型別
-
typeof
:返回資料型別的字串表達,可以判斷undefined
、數值、字串、布林值、function
,但是判斷null
、Object
、Array
時,都會返回object
-
instanceof
:判斷物件的具體型別 -
===
:可以判斷undefined
null
主要使用以上3種方法來判斷資料型別。
- 基本資料型別比較,主要用到
typeof
和===
:
var a; console.log(a, typeof a); // undefined "undefined" console.log(typeof a === undefined, typeof a === 'undefined'); // false true console.log(undefined === 'undefined'); // false console.log(a === undefined); // true a = Math.PI; console.log(typeof a === 'number'); // true console.log(typeof a === 'Number'); // false a = true; console.log(typeof a === 'boolean'); // true a = 'hello'; console.log(typeof a === 'string'); // true a = null; console.log(typeof a, typeof a === 'null'); // object false console.log(a === null); // true
- 判斷物件的具體型別:
var b = {
arr: [123, console.log, true, undefined, 'hello'],
func: function() {
return 'test';
}
};
console.log(b instanceof Object); // true
console.log(b.arr instanceof Object, b.arr instanceof Array); // true true
console.log(b.func instanceof Object, b.func instanceof Function); // true true
console.log(typeof b.func === 'function'); // true
console.log(typeof b.arr[1] === 'function'); // true
記憶體
var a = variable;
,變數a
的記憶體中儲存的是:
- 如果
variable
是基本資料型別,儲存的就是資料 - 如果
variable
是物件,儲存的是物件的地址值 - 如果
variable
是變數,儲存的是變數的記憶體內容,可能是基本資料,也可能是地址
函式傳引數,相當於把變數的內容複製給引數。
function test(num) {
num = 10;
}
var num = 20;
console.log(num); // 20
test(num);
console.log(num); // 20
此時,變數person
的內容,即物件的地址被複制給了引數,所以物件的值被修改了。
var person = {
name: 'Lee',
age: 20
};
function changeAge(obj, newAge) {
obj.age = newAge;
}
console.log(person); // {name: "Lee", age: 20}
changeAge(person, 30);
console.log(person); // {name: "Lee", age: 30}
所以JS中,函式的呼叫是值傳遞,在傳遞物件引數時,值的內容是物件的引用。
JS引擎對記憶體的管理:
- 記憶體生命週期:
- 分配小記憶體空間,得到使用權
- 儲存資料,可以多次操作
- 釋放小記憶體空間
- 釋放記憶體:
- 區域性變數:函式執行完自動釋放
- 物件:成為垃圾物件,等待垃圾回收器回收
function fn() {
var a = {};
}
fn(); // a自動釋放, a所指向的物件在之後的某個時刻被垃圾回收器回收
// console.log(a); // ReferenceError: Can't find variable: a
分號
JS語句可以不加分號,但是在以下兩種情況下會出現問題:
- 小括號開頭的前一條語句
- 中括號開頭的前一條語句
可以在行首加分號來解決該問題:
var a = 3
;(function() {
console.log('func');
})()
var b = '123'
;[1, 'hello', true, 5].forEach(function(item, index){
console.log(item, index)
})
物件
訪問物件內部資料的方式:
-
物件.屬性名,不通用
-
物件['屬性名'],通用
obj = {
name: 'object',
123: 456
};
console.log(obj.name); // object
console.log(obj['123']); // 456
必須採取方式二的情況:
- 屬性名不符合變數命名規範
- 屬性名不確定
var attr = '123';
console.log(obj[attr]); // 456
函式
函式呼叫
呼叫函式的方法:
- 直接呼叫:
test();
- 通過物件呼叫:
obj.test();
- 使用
new
呼叫:new test();
- 臨時讓函式成為物件的方法進行呼叫:
test.apply(obj);
或test.call(obj);
// obj.test(); // 報錯, 因為obj沒有test方法
test.apply(obj);
console.log(obj.hello); // Hello!
// obj.test(); // 報錯, 因為apply只是臨時呼叫
-
apply()
只接收一個數組作為引數,而call()
可以接收多個引數
this
this
是什麼:
- 所有函式內部都有一個變數
this
- 它的值是呼叫函式的當前物件
- 任何函式本質上都是通過某個物件呼叫的,如果沒有指定則為
window
物件
確定this
的值:
-
test()
:window
-
obj.test()
:obj
-
new test()
: 新建立的物件 -
test.call(obj, '123')
:obj
函式進階
原型與原型鏈
原型prototype
- 每個函式都有一個
prototype
屬性,它預設指向一個Object
空物件 - 給
prototype
新增屬性 (一般是方法),可以讓函式所有例項物件自動擁有原型中的屬性
function test() {
console.log('test');
}
console.log(test.prototype);
/* Object{
constructor: ƒ test()
[[Prototype]]: Object
} */
console.log(Date.prototype);
/* Object{
constructor: ƒ Date()
getDate: ƒ getDate()
getDay: ƒ getDay()
...
[[Prototype]]: Object
} */
- 原型物件有一個屬性
constructor
,它指向函式物件
console.log(test.prototype.constructor);
/* function test() {
console.log('test');
} */
test.prototype.constructor(); // test
console.log(Date.prototype.constructor === Date); // true
- 向原型物件新增屬性:
function Test() {
}
Test.prototype.name = 'test!';
var test = new Test();
console.log(test.name); // test!
顯式原型與隱式原型
- 每個函式都有一個
prototype
屬性,即顯式原型,預設值是一個空Object
的例項物件,Object
除外 - 每個例項物件都有一個
__proto__
屬性,即隱式原型,預設值為建構函式的prototype
屬性
繼續使用上面的例子:
console.log(test.__proto__); // Test {name: "test!"}
console.log(test.__proto__ === Test.prototype); // true
從上面的輸出結果可以發現:
- 物件的隱式原型的值即為其對應的建構函式的顯示原型的值,它們儲存了原型物件的地址值,即指向同一個原型物件
因此,給原型物件新增屬性,可以讓函式所有例項物件自動擁有原型中的屬性。
原型鏈
在訪問一個物件的屬性時:
- 首先在自身屬性中查詢,找到返回
- 如果沒有,在
__proto__
中查詢,找到返回 - 如果沒有,則返回
undefined
function Fn() {
this.test1 = function() {
console.log('test1()');
}
}
Fn.prototype.test2 = function() {
console.log('test2()');
};
func = new Fn();
func.test1(); // test1()
func.test2(); // test2()
console.log(func.test3); // undefined
上面的物件擁有的toString()
等方法,是從Object.prototype
中獲取的:
console.log(func.toString()); // [object Object]
console.log(Fn.prototype.__proto__ === Object.prototype); // true
console.log(func.toString === Object.prototype.toString); // true
原型鏈的盡頭是:Object.prototype.__proto__
console.log(Object.prototype.__proto__); // null
於是再看一下上面的這句話:
每個函式都有一個
prototype
屬性,即顯式原型,預設值是一個空Object
的例項物件,Object
除外
console.log(Fn.prototype instanceof Object); // true
console.log(Object.prototype instanceof Object); // false
- 所有函式都是
Function
的例項,包括Function
自身:
console.log(Function.__proto__ === Function.prototype); // true
console.log(Fn.__proto__ === Function.prototype); // true
物件的屬性
- 讀取物件的屬性值時,自動到原型鏈中查詢
- 設定物件的屬性值時,不會查詢原型鏈,而是直接在當前物件中設定屬性
- 方法一般定義在原型中,屬性一般通過建構函式定義在物件本身上
instanceof
- 對於
a instanceof A
而言:如果A
函式的顯式物件A.prototype
在a
物件的原型鏈上,即a.__proto__
,a.__proto__.__proto__
等,則返回true
,否則返回false
。
function Fn() {
}
var func = new Fn();
console.log(func instanceof Fn); // true
console.log(func instanceof Object); // true
console.log(func.__proto__ === Fn.prototype); // true
console.log(func.__proto__.__proto__ === Object.prototype); // true
-
Function
是通過new
自己產生的例項
function Fn() {
}
console.log(Fn instanceof Function); // true
console.log(Fn.__proto__ === Function.prototype); // true
console.log(Fn instanceof Object); // true
console.log(Fn.__proto__.__proto__ === Object.prototype); // true
-
Function
和Object
的關係:
console.log(Object instanceof Function); // true
console.log(Object instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function.prototype === Function.__proto__); // true
console.log(Object.prototype === Function.__proto__.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
執行上下文與執行上下文棧
變數宣告提升與函式宣告提升
- 變數宣告提升:使用
var
宣告的變數,在定義語句之前就可以訪問到,值為undefined
function func() {
console.log(a);
var a = 1;
}
func(); // undefined
上面的函式等價於:
function func() {
var a;
console.log(a);
a = 1;
}
- 函式宣告提升:使用
function
宣告的函式,在定義之前就可以直接呼叫
執行上下文
程式碼分為:全域性程式碼、函式 (區域性) 程式碼
- 全域性執行上下文
- 在執行程式碼之前將
window
確定為全域性執行上下文 - 對全域性資料進行預處理:
var
定義的全域性變數新增為window
的屬性,值為undefined
;function
宣告的全域性函式新增為window
的方法 -
this
賦值為window
- 在執行程式碼之前將
- 函式執行上下文
- 呼叫函式,準備執行函式之前,建立對應函式的執行上下文物件 (虛擬物件,存在於棧中)
- 對區域性資料進行預處理:形參變數賦值,新增為執行上下文的屬性;
arguments
賦值,新增為執行上下文的屬性;var
定義的區域性變數,賦值為undefined
,新增為執行上下文的屬性;function
宣告的函式,新增為執行上下文的方法 -
this
賦值為呼叫函式的物件 - 開始執行函式程式碼
function f1(a1) {
console.log(a1); // 123
console.log(a2); // undefined
f2(); // f2!
console.log(this); // Window {Infinity: Infinity, window: Window …}
console.log(arguments); // Arguments [123, 456]
var a2 = 111;
function f2() {
console.log('f2!');
}
}
f1(123, 456);
執行上下文棧
- 全域性程式碼執行前,JS引擎在記憶體中建立一個棧來儲存管理所有的執行上下文物件
- 在全域性執行上下文確定後,將其壓入棧中
- 在函式執行上下文建立後,將其壓入棧中
- 當前函式執行完,將棧頂的上下文物件彈出
- 所有程式碼執行完之後,棧中只剩下
window
作用域與作用域鏈
作用域就是一個程式碼段所在的區域,它是靜態的 (相對於上下文物件) ,在編寫程式碼時就確定了。分類:
- 全域性作用域
- 函式作用域
- 塊作用域 (ES6)
作用域用來隔離變數, 不同作用域下的同名變數不會衝突。
var a = 10;
b = 20;
function f1(x) {
var a = 100, c = 300;
console.log('f1:', a, b, c, x);
function f2(x) {
var a = 1000, d = 400;
console.log('f2:', a, b, c, d, x);
}
f2(100);
f2(200);
}
f1(10);
// f1: 100 20 300 10
// f2: 1000 20 300 400 100
// f2: 1000 20 300 400 200
作用域與執行上下文:
- 全域性作用域之外,每個函式都有自己的作用域,作用域在函式定義時已經確定,而不是在函式呼叫時;全域性執行上下文環境實在全域性作用域確定之後,JS程式碼執行前建立;函式執行上下文是在呼叫函式時,函式程式碼執行前建立
- 作用域是靜態的,只要函式定義好了就一直存在,且不會變化;執行上下文是動態的,呼叫函式時建立,函式呼叫結束自動釋放
- 上下文環境物件從屬於所在的作用域
閉包
迴圈遍歷給按鈕加監聽:
var btns = document.getElementsByTagName('button');
var len = btns.length;
for(var i = 0; i < len; i++) {
btns[i].index = i;
btns[i].onclick = function() {
console.log(this.index + 1);
};
}
使用閉包也能實現這樣的效果:
for (var i = 0; i < len; i++) {
(function (i) {
var btn = btns[i];
btn.onclick = function () {
console.log(i + 1);
}
})(i);
}
閉包的理解
如何產生閉包:
- 當一個巢狀的內部 (子) 函式引用了巢狀的外部 (父) 函式的變數時,就產生了閉包。
產生閉包的條件:
- 函式巢狀
- 內部函式引用了外部函式的資料 (變數/函式)
閉包是什麼:
- 閉包存在於巢狀的內部函式中
- 閉包是包含被引用變數/函式的物件
例如:
function f1() {
var a = 123;
var b = 456;
function f2() {
console.log(a);
}
f2(); // 需要執行內部函式
}
f1();
當斷點在var b = 456;
時,
![image-20220307221012692](/Users/menghuixiao/Library/Application Support/typora-user-images/image-20220307221012692.png)
[[Scopes]]: Scopes[2]
0: Closure (f1)
a: 123
1: Global {window: Window, self: Window, ...}
當斷點在console.log(a);
時,
![image-20220307221036615](/Users/menghuixiao/Library/Application Support/typora-user-images/image-20220307221036615.png)
閉包 (f1)
a: 123
常見的閉包
- 將函式作為巢狀的外部函式的返回值
- 將函式作為實參傳遞給另一個函式呼叫
情況1:
function f1() {
var a = 1;
function f2() {
a++;
console.log(a);
}
return f2;
}
var test1 = f1(); // 產生一個閉包
test1(); // 2
test1(); // 3
test1 = null; // 閉包死亡 (包含閉包的函式稱為垃圾物件)
var test2 = f1(); // 又產生一個閉包
test2(); // 2
test2(); // 3
函式f1()
中的var a
本來是函式作用域,當函式被呼叫之後,應當被垃圾回收器回收,但是由於有閉包,它一直存在,並且每次呼叫test1()
都能自增。
情況2:
function show(msg, delay) {
setTimeout(function() {
console.log(msg);
}, delay);
}
show('Hello', 1000); // Hello
閉包的作用
- 使函式內部的變數在函式執行完之後,仍然存活在函式中,延長區域性變數的生命週期
- 讓函式外部可以讀寫函式內部的資料
函式執行完畢之後,一般來說,函式內部宣告的區域性變數被釋放,除非變數存在於閉包中。另外,在函式外部無法訪問函式內部的區域性變數,但是可以通過閉包來操作。
閉包的應用:自定義JS模組
通過閉包實現模組,好處有:
- 將所有資料和功能都封裝在一個函式內部
- 只向外暴露一個包含若干個方法的物件
- 模組的使用者只需要呼叫物件的方法即能實現相應的功能
/* 1.js */
function myMath() {
var pi = 3.14; // 私有資料
function area(radius) {
return pi * radius * radius;
}
// 向外部暴露的方法
return {
area: area
};
}
/* 2.js */
var module = myMath();
console.log(module.area(10)); // 314
此時:
閉包 (myMath)
pi: 3.14
閉包的缺點
- 函式執行完畢,函式內的佈局變數沒有釋放,佔用記憶體時間較長
- 容易造成記憶體洩漏
- 記憶體溢位:程式需要的記憶體超過了需要的記憶體
- 記憶體洩漏:佔用的記憶體沒有及時釋放,當記憶體洩漏多了,就容易產生記憶體溢位
- 常見的記憶體洩漏:
- 意外的全域性變數
- 沒有及時清理的計時器或回撥函式
- 閉包
所以在使用閉包時,注意及時手動釋放記憶體。
面向物件進階
物件建立模式
Object建構函式模式
- 先建立空的
Object
物件 - 動態新增屬性方法
- 使用場景:建立物件時不確定物件內部資料
- 問題:語句太多
let p = new Object();
p.name = 'Lee';
p['age'] = 19;
p.setAge = function(age) {
p.age = age;
}
console.log(p); // {name: "Lee", age: 19, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
物件字面量模式
- 使用
{}
建立物件,同時指定屬性和方法 - 使用場景:建立物件時,物件資料是確定的
- 問題:建立多個物件時有重複程式碼
let p = {
name: 'Lee',
age: 20,
setAge: function(age) {
this.age = age;
}
};
console.log(p); // {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
工廠模式
- 通過工廠函式動態建立物件並返回
- 使用場景:需要建立多個物件
- 問題:物件沒有具體型別,都是
Object
型別
function createPerson(name, age) {
var p = {
name: name,
age: age,
setAge: function (age) {
this.age = age;
}
};
return p;
}
p = createPerson('Lee', 20);
console.log(p); // {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
自定義建構函式模式
- 自定義建構函式,通過
new
建立物件 - 使用場景:需要建立多個同一型別的物件
- 問題:每個物件都有相同的資料 (方法),浪費記憶體,需要放到原型物件中
function Person(name, age) {
this.name = name;
this.age = age;
this.setAge = function (age) {
this.age = age;
};
}
p = new Person('Lee', 20);
console.log(p); // Person {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // Person {name: "Lee", age: 30, setAge: function}
建構函式+原型模式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setAge = function (age) {
this.age = age;
};
p = new Person('Lee', 20);
console.log(p); // Person {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // Person {name: "Lee", age: 30, setAge: function}
繼承模式
原型鏈繼承
原理:子型別的原型為父型別的一個例項物件
- 定義父型別構造方法
- 給父型別的原型新增方法
- 定義子型別建構函式
- 建立父型別物件,並賦值給子型別的原型
- 將子型別原型的構造屬性 (
constructor
) 設定為子型別 - 給子型別原型新增方法
- 建立子型別物件:繼承父型別的方法
// 1. 定義父型別構造方法
function Super() {
this.superProp = 'SuperProp!';
}
// 2. 給父型別的原型新增方法
Super.prototype.showSuperProp = function() {
console.log(this.superProp);
}
// 3. 定義子型別建構函式
function Sub() {
this.subProp = 'SubProp!';
}
// 4. 建立父型別物件,並賦值給子型別的原型
Sub.prototype = new Super();
// 5. 將子型別原型的構造屬性 (constructor) 設定為子型別
Sub.prototype.constructor = Sub;
// 6. 給子型別原型新增方法
Sub.prototype.showSubProp = function() {
console.log(this.subProp);
}
var sub = new Sub();
sub.showSubProp(); // SubProp!
sub.showSuperProp(); // SuperProp!
console.log(sub.toString()); // [object Object]
借用建構函式繼承
- 定義父型別建構函式
- 定義子型別建構函式
- 在子型別建構函式中呼叫父型別建構函式
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
組合繼承
結合上面兩種方法:
- 利用原型鏈繼承父類的方法
- 利用父型別建構函式,為子型別初始化相同的屬性
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log('I am ' + this.name);
};
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
console.log('Grade: ' + this.grade);
};
var s = new Student('Lee', 20, 9);
console.log(s.name, s.age, s.grade); // Lee 20 9
s.sayName(); // I am Lee
s.sayGrade(); // Grade: 9
執行緒機制與事件機制
程序與執行緒
- 程序process:程式的一次執行,它佔有一片獨有的記憶體空間
- 執行緒thread:程序內的一個獨立執行單元,程式執行的一個完整流程,CPU排程的最小單元
- 執行緒池tread pool:儲存多個執行緒物件的容器,實現執行緒物件的反覆利用
應用程式必須執行在某個程序的某個執行緒上。一個程序中至少有一個執行的執行緒:主執行緒,程序啟動之後自動建立。程序內的資料可以由其中的多個執行緒直接共享,程序之間的資料不能直接共享。
多程序與多執行緒:
- 多程序:一個應用程式可以同時啟動多個例項;多執行緒:一個程序中,同時有多個執行緒執行;
多執行緒與單執行緒:
- 多執行緒的優點是能有效提升CPU利用率,缺點是建立多執行緒的開銷、執行緒切換的開銷、死鎖和狀態同步的問題;單執行緒的優點是順序程式設計簡單,缺點是效率低、
JS:
- JS是單執行緒執行的
- 使用H5的Web Workers可以多執行緒執行
瀏覽器:
- 多執行緒執行
瀏覽器核心
瀏覽器核心是支撐瀏覽器執行的最核心程式:
- Chrome、Safari:webkit
- firefox:Gecko
瀏覽器核心由許多模組組成:
- 主執行緒:
- JS引擎模組:負責JS程式的編譯與執行
- HTML、CSS文件解析模組:負責頁面文字的解析
- DOM/CSS模組:負責DOM和CSS在記憶體中的處理
- 佈局和渲染模組:負責頁面的佈局和效果的繪製
- 分執行緒:
- 定時器模組:負責定時器的管理
- 事件響應模組:負責事件的管理
- 網路請求模組:負責ajax請求
定時器
- 定時器不能保證真正的定時執行,一般會延遲一點時間
- 定時器的回撥函式在主執行緒中執行
- 定時器的實現:事件迴圈模型