1. 程式人生 > 其它 >前端 - JS進階

前端 - JS進階

JS文件

目錄

基礎總結

資料型別

  • 基本資料型別:
    • String
    • Number
    • boolean
    • undefined
    • null
  • 物件 (引用) 型別:
    • Object:任意物件
    • Function:特殊的物件,可以執行
    • Array:特殊的物件,內部資料有序,數值索引

判斷資料型別

  1. typeof:返回資料型別的字串表達,可以判斷undefined、數值、字串、布林值、function,但是判斷nullObjectArray時,都會返回object
  2. instanceof:判斷物件的具體型別
  3. ===:可以判斷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引擎對記憶體的管理:

  • 記憶體生命週期:
    1. 分配小記憶體空間,得到使用權
    2. 儲存資料,可以多次操作
    3. 釋放小記憶體空間
  • 釋放記憶體:
    1. 區域性變數:函式執行完自動釋放
    2. 物件:成為垃圾物件,等待垃圾回收器回收
function fn() {
    var a = {};
}
fn();  // a自動釋放, a所指向的物件在之後的某個時刻被垃圾回收器回收

// console.log(a);  // ReferenceError: Can't find variable: a

分號

JS語句可以不加分號,但是在以下兩種情況下會出現問題:

  1. 小括號開頭的前一條語句
  2. 中括號開頭的前一條語句

可以在行首加分號來解決該問題:

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

必須採取方式二的情況:

  1. 屬性名不符合變數命名規範
  2. 屬性名不確定
var attr = '123';
console.log(obj[attr]);  // 456

函式

函式呼叫

呼叫函式的方法:

  1. 直接呼叫:test();
  2. 通過物件呼叫:obj.test();
  3. 使用new呼叫:new test();
  4. 臨時讓函式成為物件的方法進行呼叫: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是什麼:

  1. 所有函式內部都有一個變數this
  2. 它的值是呼叫函式的當前物件
  3. 任何函式本質上都是通過某個物件呼叫的,如果沒有指定則為window物件

確定this的值:

  1. test(): window
  2. obj.test(): obj
  3. new test(): 新建立的物件
  4. 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!

顯式原型與隱式原型

  1. 每個函式都有一個prototype屬性,即顯式原型,預設值是一個空Object的例項物件,Object除外
  2. 每個例項物件都有一個__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

物件的屬性

  1. 讀取物件的屬性值時,自動到原型鏈中查詢
  2. 設定物件的屬性值時,不會查詢原型鏈,而是直接在當前物件中設定屬性
  3. 方法一般定義在原型中,屬性一般通過建構函式定義在物件本身上

instanceof

  • 對於a instanceof A而言:如果A函式的顯式物件A.prototypea物件的原型鏈上,即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
  • FunctionObject的關係:
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的屬性,值為undefinedfunction宣告的全域性函式新增為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);

執行上下文棧

  1. 全域性程式碼執行前,JS引擎在記憶體中建立一個棧來儲存管理所有的執行上下文物件
  2. 在全域性執行上下文確定後,將其壓入棧中
  3. 在函式執行上下文建立後,將其壓入棧中
  4. 當前函式執行完,將棧頂的上下文物件彈出
  5. 所有程式碼執行完之後,棧中只剩下window

作用域與作用域鏈

作用域就是一個程式碼段所在的區域,它是靜態的 (相對於上下文物件) ,在編寫程式碼時就確定了。分類:

  1. 全域性作用域
  2. 函式作用域
  3. 塊作用域 (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

作用域與執行上下文:

  1. 全域性作用域之外,每個函式都有自己的作用域,作用域在函式定義時已經確定,而不是在函式呼叫時;全域性執行上下文環境實在全域性作用域確定之後,JS程式碼執行前建立;函式執行上下文是在呼叫函式時,函式程式碼執行前建立
  2. 作用域是靜態的,只要函式定義好了就一直存在,且不會變化;執行上下文是動態的,呼叫函式時建立,函式呼叫結束自動釋放
  3. 上下文環境物件從屬於所在的作用域

閉包

迴圈遍歷給按鈕加監聽:

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);
}

閉包的理解

如何產生閉包:

  • 當一個巢狀的內部 (子) 函式引用了巢狀的外部 (父) 函式的變數時,就產生了閉包。

產生閉包的條件:

  1. 函式巢狀
  2. 內部函式引用了外部函式的資料 (變數/函式)

閉包是什麼:

  • 閉包存在於巢狀的內部函式中
  • 閉包是包含被引用變數/函式的物件

例如:

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. 將函式作為巢狀的外部函式的返回值
  2. 將函式作為實參傳遞給另一個函式呼叫

情況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

閉包的作用

  1. 使函式內部的變數在函式執行完之後,仍然存活在函式中,延長區域性變數的生命週期
  2. 讓函式外部可以讀寫函式內部的資料

函式執行完畢之後,一般來說,函式內部宣告的區域性變數被釋放,除非變數存在於閉包中。另外,在函式外部無法訪問函式內部的區域性變數,但是可以通過閉包來操作。

閉包的應用:自定義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

閉包的缺點

  1. 函式執行完畢,函式內的佈局變數沒有釋放,佔用記憶體時間較長
  2. 容易造成記憶體洩漏
    • 記憶體溢位:程式需要的記憶體超過了需要的記憶體
    • 記憶體洩漏:佔用的記憶體沒有及時釋放,當記憶體洩漏多了,就容易產生記憶體溢位
  • 常見的記憶體洩漏:
    • 意外的全域性變數
    • 沒有及時清理的計時器或回撥函式
    • 閉包

所以在使用閉包時,注意及時手動釋放記憶體。

面向物件進階

物件建立模式

Object建構函式模式

  1. 先建立空的Object物件
  2. 動態新增屬性方法
  3. 使用場景:建立物件時不確定物件內部資料
  4. 問題:語句太多
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}

物件字面量模式

  1. 使用{}建立物件,同時指定屬性和方法
  2. 使用場景:建立物件時,物件資料是確定的
  3. 問題:建立多個物件時有重複程式碼
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}

工廠模式

  1. 通過工廠函式動態建立物件並返回
  2. 使用場景:需要建立多個物件
  3. 問題:物件沒有具體型別,都是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}

自定義建構函式模式

  1. 自定義建構函式,通過new建立物件
  2. 使用場景:需要建立多個同一型別的物件
  3. 問題:每個物件都有相同的資料 (方法),浪費記憶體,需要放到原型物件中
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}

繼承模式

原型鏈繼承

原理:子型別的原型為父型別的一個例項物件

  1. 定義父型別構造方法
  2. 給父型別的原型新增方法
  3. 定義子型別建構函式
  4. 建立父型別物件,並賦值給子型別的原型
  5. 將子型別原型的構造屬性 (constructor) 設定為子型別
  6. 給子型別原型新增方法
  7. 建立子型別物件:繼承父型別的方法
// 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]

借用建構函式繼承

  1. 定義父型別建構函式
  2. 定義子型別建構函式
  3. 在子型別建構函式中呼叫父型別建構函式
function Person(name, age) {
    this.name = name;
    this.age = age;
}

function Student(name, age, grade) {
    Person.call(this, name, age);
    this.grade = grade;
}

組合繼承

結合上面兩種方法:

  1. 利用原型鏈繼承父類的方法
  2. 利用父型別建構函式,為子型別初始化相同的屬性
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請求

定時器

  • 定時器不能保證真正的定時執行,一般會延遲一點時間
  • 定時器的回撥函式在主執行緒中執行
  • 定時器的實現:事件迴圈模型