JavaScript 進階知識 - 高級篇

image

JS高級

前言

經過前面幾篇文章的學習,相信大家已經對js有了大部分的理解了,但是要想真正的掌握好js,本篇纔是關鍵。由於js高級階段的知識點比較難理解,所以本篇文章花了大量的時間去理思路,有可能有一些知識點遺漏了,也有肯能有部分知識點寫的不對,歡迎大家留言糾正。另外,大家在以後的學習中千萬不要被一些難點所嚇到,聽說有些知識點很難,其實並不是真正的難,只要你靜下心慢慢的理解,其實還是很簡單的。

1.異常處理

常見的異常分類

  • 運行環境的多樣性導致的異常(瀏覽器)
  • 語法錯誤,代碼錯誤

異常最大的特徵,就是一旦代碼出現異常,後面的代碼就不會執行。

1.1異常捕獲

捕獲異常,使用try-catch語句:
try{
    // 這裏寫可能出現異常的代碼
}catch(e){
    // e-捕獲的異常對象
    // 可以在此處書寫出現異常後的處理代碼
}
異常捕獲語句執行的過程爲:
  • 代碼正常運行, 如果在try中出現了錯誤,try裏面出現錯誤的語句後面的代碼都不再執行, 直接跳轉到catch
  • catch中處理錯誤信息
  • 然後繼續執行後面的代碼
  • 如果try中沒有出現錯誤, 那麼不走catch直接執行後面的代碼

通過try-catch語句進行異常捕獲之後,代碼將會繼續執行,而不會中斷。

示例代碼:

console.log('代碼開始執行');
try{
    console.log(num); // num 在外部是沒有定義的
}catch(e){
    console.log(e);
    console.log('我已經把錯誤處理了');
}
console.log('代碼結束執行');

效果圖:

image

從效果圖中我們可以看到,num是一個沒有定義的變量,如果沒有放在try-catch代碼塊中,後面的‘代碼結束執行’就不會被打印。通過把try-catch放在代碼塊中,出現錯誤後,就不會影響後面代碼的運行了,他會把錯誤信息打印出來。

注意:

  • 語法錯誤異常用try-catch語句無法捕獲,因爲在預解析階段,語法錯誤會直接檢測出來,而不會等到運行的時候才報錯。
  • try-catch在一般日常開發中基本用不到,但是如果要寫框架什麼的,用的會非常多。因爲這個會讓框架變得健壯

異常捕獲語句的完整模式

異常捕獲語句的完整模式爲try-catch-finally
try {
    //可能出現錯誤的代碼
} catch ( e ) {
    //如果出現錯誤就執行
} finally {
    //結束 try 這個代碼塊之前執行, 即最後執行
}

finally中的代碼,不管有沒有發生異常,都會執行。一般用在後端語言中,用來釋放資源,JavaScript中很少會用到

1.2拋出異常

如何手動的拋出異常呢?

案例:自己寫的一個函數,需要一個參數,如果用戶不傳參數,此時想直接給用戶拋出異常,就需要了解如何拋出異常。

拋出異常使用throw關鍵字,語法如下:

throw 異常對象;

異常對象一般是用new Error("異常消息"), 也可以使用任意對象

示例代碼:

function test(para){
    if(para == undefined){
        throw new Error("請傳遞參數");
        //這裏也可以使用自定義的對象
        throw {"id":1, msg:"參數未傳遞"};
    }
}

try{
    test();
}catch(e){
    console.log(e);
}

效果圖:

image

image

1.3異常的傳遞機制

function f1 () {
    f2(); 
}

function f2 () {
    f3();
}

function f3() {
    throw new Error( 'error' );
}
f1();  // f1 稱爲調用者, 或主調函數, f2 稱爲被調用者, 或被調函數

當在被調函數內發生異常的時候,異常會一級一級往上拋出。

2.面向對象編程

在瞭解面向對象編程之前,我們先來了解下什麼是面向過程,什麼是面向對象,他們之間的區別是什麼。

2.1 面向過程和麪向對象的的對比

舉個例子:

日常洗衣服

1.面向過程的思維方式:

面向過程編程:將解決問題的關注點放在解決問題的具體細節上,關注如何一步一步實現代碼細節;
step 1:收拾髒衣服
step 2:打開洗衣機蓋
step 3:將髒衣服放進去
step 4:設定洗衣程序
step 5:開始洗衣服
step 6:打開洗衣機蓋子
step 7:曬衣服

2.面向對象的思維方式:

面向對象編程:將解決問題的關注點放在解決問題所需的對象上,我們重點找對象;
人(對象)
洗衣機(對象)

在面向對象的思維方式中:我們只關心要完成事情需要的對象,面向對象其實就是對面向過程的封裝;

示例代碼:

在頁面上動態創建一個元素
//面向過程
//1-創建一個div
var  div=document.createElement('div');
//2-div設置內容
div.innerHTML='我是div';
//3-添加到頁面中
document.body.appendChild(div);

//面向對象
$('body').append('<div>我也是div</div>');

我們可以看出,jQ封裝的其實就是對面向過程的封裝。

總結: 面向對象是一種解決問題的思路,一種編程思想。

2.2 面向對象編程舉例

設置頁面中的divp的邊框爲'1px solid red'

1、傳統的處理辦法

// 1> 獲取div標籤
var divs = document.getElementsByTagName( 'div' );
// 2> 遍歷獲取到的div標籤
for(var i = 0; i < divs.length; i++) {
    //3> 獲取到每一個div元素,設置div的樣式
    divs[i].style.border = "1px dotted black";
}

// 4> 獲取p標籤
var ps = document.getElementsByTagName("p");
// 5> 遍歷獲取到的p標籤
for(var j = 0; j < ps.length; j++) { 
    // 獲取到每一個p元素 設置p標籤的樣式
    ps[j].style.border = "1px dotted black"; 
}

2、使用函數進行封裝優化

// 通過標籤名字來獲取頁面中的元素 
function tag(tagName) { 
    return document.getElementsByTagName(tagName); 
}

// 封裝一個設置樣式的函數 
function setStyle(arr) { 
    for(var i = 0; i < arr.length; i++) { 
        // 獲取到每一個div或者p元素 
        arr[i].style.border = "1px solid #abc"; 
    } 
}
var dvs = tag("div");
var ps = tag("p");
setStyle(dvs); 
setStyle(ps);

3、使用面向對象的方式

// 更好的做法:是將功能相近的代碼放到一起 
var obj = {     // 命名空間
    getEle: { 
        tag: function (tagName) { 
            return document.getElementsByTagName(tagName); 
        }, 
        id: function (idName) { 
            return document.getElementById(idName); 
        } 
        // ...
    },    
    setCss: { 
        setStyle: function (arr) { 
            for(var i = 0; i < arr.length; i++) { 
                arr[i].style.border = "1px solid #abc"; 
            } 
        }, 
        css: function() {}, 
        addClass: function() {}, 
        removeClass: function() {} 
        // ... 
    } 
    // 屬性操作模塊 
    // 動畫模塊 
    // 事件模塊 
    // ... 
};

var divs = obj.getEle.tag('div');
obj.setCss.setStyle(divs);

2.3 面向對象的三大特性

面向對象的三大特性分別是:'封裝''繼承''多態'

1、封裝性

對象就是對屬性和方法的封裝,要實現一個功能,對外暴露一些接口,調用者只需通過接口調用即可,不需要關注接口內部實現原理。
  • js對象就是“鍵值對”的集合

    • 鍵值如果是數據( 基本數據, 複合數據, 空數據 ), 就稱爲屬性
    • 如果鍵值是函數, 那麼就稱爲方法
  • 對象就是將屬性與方法封裝起來
  • 方法是將過程封裝起來

2、繼承性

所謂繼承就是自己沒有, 別人有,拿過來爲自己所用, 併成爲自己的東西

2.1、傳統繼承基於模板

子類可以使用從父類繼承的屬性和方法。

class Person {
 string name;
 int age;
}

class Student : Person {
}
var stu = new Student();
stu.name

即:讓某個類型的對象獲得另一個類型的對象的屬性的方法

2.2、js 繼承基於對象

JavaScript中,繼承就是當前對象可以使用其他對象的方法和屬性。

js繼承實現舉例:混入(mix

// 參數o1和o2是兩個對象,其中o1對象繼承了所有o2對象的“k”屬性或者方法
var o1 = {};
var o2 = {
    name: 'Levi',
    age: 18,
    gender: 'male'
};
function mix ( o1, o2 ) {
    for ( var k in o2 ) {
        o1[ k ] = o2[ k ];
    }
}
mix(o1, o2);
console.log(o1.name); // "Levi"

3、多態性(基於強類型,js中沒有多態)只做瞭解

同一個類型的變量可以表現出不同形態,用父類的變量指向子類的對象。
動物 animal = new 子類(); // 子類:麻雀、狗、貓、豬、狐狸...
動物 animal = new 狗();
animal.叫();

2.4 創建對象的方式

1、字面量 {}

var student1 = {
    name:'諸葛亮',
    score:100,
    code:1,
}

var student2 = {
    name:'蔡文姬',
    score:98,
    code:2,
}

var student3 = {
    name:'張飛',
    score:68,
    code:3,
}

字面量創建方式,代碼複用性太低,每一次都需要重新創建一個對象。

2、Object()構造函數

var student1 = new Object();
    student1.name = '諸葛亮';
    student1.score = 100;
    student1.code = 1;

var student2 = new Object();
    student2.name = '蔡文姬';
    student2.score = 98;
    student2.code = 2;
    
var student3 = new Object();
    student3.name = '張飛';
    student3.score = 68;
    student3.code = 3;

代碼複用性太低,字面量創建的方式其實就是代替Object()構造函數創建方式的。

3、自定義構造函數

自定義構造函數,可以快速創建多個對象,並且代碼複用性高。
// 一般爲了區分構造函數與普通函數,構造函數名首字母大寫
function Student(name,score,code){  
    this.name = name;
    this.score = score;
    this.code = code;
}

var stu1 = new Student('諸葛亮',100,1);
var stu2 = new Student('蔡文姬',98,2);
var stu3 = new Student('張飛',68,3);

構造函數語法:

  • 構造函數名首字母大寫;
  • 構造函數一般與關鍵字:new一起使用;
  • 構造函數一般不需要設置return語句,默認返回的是新創建的對象;
  • this指向的是新創建的對象。

構造函數的執行過程:

  • new關鍵字,創建一個新的對象,會在內存中開闢一個新的儲存空間;
  • 讓構造函數中的this指向新創建的對象;
  • 執行構造函數,給新創建的對象進行初始化(賦值);
  • 構造函數執行(初始化)完成,會將新創建的對象返回。

構造函數的注意點:

  • 構造函數本身也是函數;
  • 構造函數有返回值,默認返回的是新創建的對象;
  • 但是如果手動添加返回值,添加的是值類型數據的時候,構造函數沒有影響。如果添加的是引用類型(數組、對象等)值的時候,會替換掉新創建的對象。
function Dog(){
    this.name="哈士奇";
    this.age=0.5;
    this.watch=function(){
        console.log('汪汪汪,禁止入內');
    }
    // return false;          返回值不會改變,還是新創建的對象
    // return 123;            返回值不會改變,還是新創建的對象
    // return [1,2,3,4,5];    返回值發生改變,返回的是這個數組
    return  {aaa:'bbbb'};  // 返回值發生改變,返回的是這個對象
}

var d1=new Dog();  // 新創建一個對象
console.log(d1);
  • 構造函數可以當做普通函數執行,裏面的this指向的是全局對象window
 function Dog(){
    this.name="husky";
    this.age=0.5;
    this.watch=function(){
        console.log('汪汪汪,禁止入內');
    }
    console.log(this);  // window對象
    return 1;
}
console.log(Dog());  // 打印 1

2.5 面向對象案例

通過一個案例,我們來了解下面向對象編程(案例中有一個prototype概念,可以學完原型那一章後再來看這個案例)。

需求:

  • 實現一個MP3音樂管理案例;
  • 同種類型的MP3,廠家會生產出成百上千個,但是每個MP3都有各自的樣式、使用者、歌曲;
  • 每個MP3都有一樣的播放、暫停、增刪歌曲的功能(方法);

圖解:

image

示例代碼:

    // 每個MP3都有自己的 主人:owner 樣式:color 歌曲:list
    function MP3(name,color,list){
        this.owner = name || 'Levi';  // 不傳值時默認使用者是‘Levi’
        this.color = color || 'pink';
        this.musicList = list || [
            {songName:'男人哭吧不是罪',singer:'劉德華'},
            {songName:'吻別',singer:'張學友'},
            {songName:'對你愛不完',singer:'郭富城'},
            {songName:'今夜你會不會來',singer:'黎明'}
        ];
    }
    // 所有的MP3都有 播放 暫停 音樂 增刪改查的功能
    MP3.prototype = {
        // 新增
        add:function(songName,singer){
            this.musicList.push({songName:songName,singer:singer});
        },
        // 查找
        select:function(songName){
            for(var i=0;i<this.musicList.length;i++){
                if(this.musicList[i].songName == songName){
                    return this.musicList[i];
                }
            }
            return null;  // 如果沒有搜索到返回null
        },
        // 修改
        update:function(songName,singer){
            // 先找到這首歌 在修改
            var result = this.select(songName); // 查找
            if(result){
                result.singer = singer; // 修改
            }
        },
        // 刪除
        delete:function(songName){
            // 先找到音樂  splice(index,1)
            var result = this.select(songName);
            // 知道該音樂的索引值
            // 刪除
            if(result){
               var index = this.musicList.indexOf(result);
               this.musicList.splice(index,1); // 從指定索引值來刪除數據
            }
        },
        // 顯示
        show:function(){
            console.log(this.owner+'的MP3');
            for(var i=0;i<this.musicList.length;i++){
                console.log(this.musicList[i].songName +'---'+this.musicList[i].singer);
            }
        }
    }
    
    var XiaoHong = new MP3('小紅');  // 實例小紅MP3
    var XiaoMing = new MP3('小明');  // 實例小明MP3
    var XiaoDong = new MP3('小東');  // 實例小東MP3
    
    XiaoHong.add('十年','陳奕迅');          // 小紅的歌單裏添加歌曲
    XiaoDong.add('月亮之上','鳳凰傳奇');    // 小東的歌單裏添加歌曲
    
    XiaoMing.musicList = [                  // 小明的歌單替換
        {
            songName:'精忠報國',
            singer:'屠洪剛'
        },
        {
            songName:'窗外',
            singer:'未知'
        }
    ];
    // 展示各自的歌單
    XiaoHong.show();        
    XiaoMing.show();
    XiaoDong.show();

打印結果:

image

3.原型

3.1 傳統構造函數存在問題

通過自定義構造函數的方式,創建小狗對象:

兩個實例化出來的“小狗”,它們都用的同一個say方法,爲什麼最後是false呢?
function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = function() {
        console.log('汪汪汪');
    }
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //輸出結果爲false

畫個圖理解下:

image

每次創建一個對象的時候,都會開闢一個新的空間,我們從上圖可以看出,每隻創建的小狗有一個say方法,這個方法都是獨立的,但是功能完全相同。隨着創建小狗的數量增多,造成內存的浪費就更多,這就是我們需要解決的問題。

爲了避免內存的浪費,我們想要的其實是下圖的效果:

image

解決方法:

這裏最好的辦法就是將函數體放在構造函數之外,在構造函數中只需要引用該函數即可。
function sayFn() {
    console.log('汪汪汪');
}

function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = sayFn();
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //輸出結果爲 true

這樣寫依然存在問題:

  • 全局變量增多,會增加引入框架命名衝突的風險
  • 代碼結構混亂,會變得難以維護

想要解決上面的問題就需要用到構造函數的原型概念。

3.2 原型的概念

prototype:原型。每個構造函數在創建出來的時候系統會自動給這個構造函數創建並且關聯一個空的對象。這個空的對象,就叫做原型。

關鍵點:

  • 每一個由構造函數創建出來的對象,都會默認的和構造函數的原型關聯;
  • 當使用一個方法進行屬性或者方法訪問的時候,會先在當前對象內查找該屬性和方法,如果當前對象內未找到,就會去跟它關聯的原型對象內進行查找;
  • 也就是說,在原型中定義的方法跟屬性,會被這個構造函數創建出來的對象所共享;
  • 訪問原型的方式:構造函數名.prototype

示例圖:

image

示例代碼: 給構造函數的原型添加方法

function Dog(name,age){
    this.name = name;
    this.age = age;
}

// 給構造函數的原型 添加say方法
Dog.prototype.say = function(){
    console.log('汪汪汪');
}

var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

dog1.say();  // 汪汪汪
dog2.say();  // 汪汪汪

我們可以看到,本身Dog這個構造函數中是沒有say這個方法的,我們通過Dog.prototype.say的方式,在構造函數Dog的原型中創建了一個方法,實例化出來的dog1dog2會先在自己的對象先找say方法,找不到的時候,會去他們的原型對象中查找。

如圖所示:

創建一個原型對象

在構造函數的原型中可以存放所有對象共享的數據,這樣可以避免多次創建對象浪費內存空間的問題。

3.3 原型的使用

1、使用對象的動態特性

使用對象的動態屬性,其實就是直接使用prototype爲原型添加屬性或者方法。
function Person () {}

Person.prototype.say = function () {
    console.log( '講了一句話' );
};

Person.prototype.age = 18;

var p = new Person();
p.say();  // 講了一句話
console.log(p.age);  // 18

2、直接替換原型對象

每次構造函數創建出來的時候,都會關聯一個空對象,我們可以用一個對象替換掉這個空對象。
function Person () {}

Person.prototype = {
    say : function () {
        console.log( '講了一句話' );
    },
};

var p = new Person();
p.say();  // 講了一句話

注意:

使用原型的時候,有幾個注意點需要注意一下,我們通過幾個案例來了解一下。
  • 使用對象.屬性名去獲取對象屬性的時候,會先在自身中進行查找,如果沒有,就去原型中查找;
// 創建一個英雄的構造函數 它有自己的 name 和 age 屬性
function Hero(){
    this.name="德瑪西亞之力";
    this.age=18;
}
// 給這個構造函數的原型對象添加方法和屬性
Hero.prototype.age= 30;
Hero.prototype.say=function(){
    console.log('人在塔在!!!');
}

var h1 = new Hero();
h1.say();   // 先去自身中找 say 方法,沒有再去原型中查找  打印:'人在塔在!!!'
console.log(p1.name);  // "德瑪西亞之力"
console.log(p1.age);   // 18 先去自身中找 age 屬性,有的話就不去原型中找了
  • 使用對象.屬性名去設置對象屬性的時候,只會在自身進行查找,如果有,就修改,如果沒有,就添加;
// 創建一個英雄的構造函數
function Hero(){
    this.name="德瑪西亞之力";
}
// 給這個構造函數的原型對象添加方法和屬性
Hero.prototype.age = 18;

var h1 = new Hero();
console.log(h1);       // {name:"德瑪西亞之力"}
console.log(h1.age);   // 18

h1.age = 30;           // 設置的時候只會在自身中操作,如果有,就修改,如果沒有,就添加 不會去原型中操作
console.log(h1);       // {name:"德瑪西亞之力",age:30}
console.log(h1.age);   // 30
  • 一般情況下,不會將屬性放在原型中,只會將方法放在原型中;
  • 在替換原型的時候,替換之前創建的對象,和替換之後創建的對象的原型不一致!!!
// 創建一個英雄的構造函數 它有自己的 name 屬性
function Hero(){
    this.name="德瑪西亞之力";
}
// 給這個構造函數的默認原型對象添加 say 方法
Hero.prototype.say = function(){
    console.log('人在塔在!!!');
}

var h1 = new Hero();
console.log(h1);    // {name:"德瑪西亞之力"}
h1.say();           // '人在塔在!!!'

// 開闢一個命名空間 obj,裏面有個 kill 方法
var obj = {
    kill : function(){
        console.log('大寶劍');
    }
}

// 將創建的 obj 對象替換原本的原型對象
Hero.prototype = obj;

var h2 = new Hero();

h1.say();           // '人在塔在!!!'
h2.say();           // 報錯

h1.kill();          // 報錯
h2.kill();          // '大寶劍'

畫個圖理解下:

image

圖中可以看出,實例出來的h1對象指向的原型中,只有say()方法,並沒有kill()方法,所以h1.kill()會報錯。同理,h2.say()也會報錯。

3.4 __proto__屬性

js中以_開頭的屬性名爲js的私有屬性,以__開頭的屬性名爲非標準屬性。__proto__是一個非標準屬性,最早由firefox提出來。

1、構造函數的 prototype 屬性

之前我們訪問構造函數原型對象的時候,使用的是prototype屬性:
function Person(){}

//通過構造函數的原型屬性prototype可以直接訪問原型
Person.prototype;
在之前我們是無法通過構造函數new出來的對象訪問原型的:
function Person(){}

var p = new Person();

//以前不能直接通過p來訪問原型對象

2、實例對象的 __proto__ 屬性

__proto__屬性最早是火狐瀏覽器引入的,用以通過實例對象來訪問原型,這個屬性在早期是非標準的屬性,有了__proto__屬性,就可以通過構造函數創建出來的對象直接訪問原型。
function Person(){}

var p = new Person();

//實例對象的__proto__屬性可以方便的訪問到原型對象
p.__proto__;

//既然使用構造函數的`prototype`和實例對象的`__proto__`屬性都可以訪問原型對象
//就有如下結論
p.__proto__ === Person.prototype;

如圖所示:

image

3、__proto__屬性的用途

  • 可以用來訪問原型;
  • 在實際開發中除非有特殊的需求,不要輕易的使用實例對象的__proto__屬性去修改原型的屬性或方法;
  • 在調試過程中,可以輕易的查看原型的成員;
  • 由於兼容性問題,不推薦使用。

3.5 constuctor屬性

constructor:構造函數,原型的constructor屬性指向的是和原型關聯的構造函數。

示例代碼:

function Dog(){
    this.name="husky";
}

var d=new Dog();

// 獲取構造函數
console.log(Dog.prototype.constructor);  // 打印構造函數 Dog
console.log(d.__proto__.constructor);    // 打印構造函數 Dog

如圖所示:

image

獲取複雜類型的數據類型:

通過obj.constructor.name的方式,獲取當前對象obj的數據類型。

在一個的函數中,有個返回值name,它表示的是當前函數的函數名;

function Teacher(name,age){
    this.name = name;
    this.age = age;
}

var teacher = new Teacher();

// 假使我們只知道一個對象teacher,如何獲取它的類型呢?
console.log(teacher.__proto__.constructor.name);  // Teacher

console.log(teacher.constructor.name);  // Teacher

實例化出來的teacher對象,它的數據類型是啥呢?我們可以通過實例對象teacher.__proto__,訪問到它的原型對象,再通過.constructor訪問它的構造函數,通過.name獲取當前函數的函數名,所以就能得到當前對象的數據類型。又因爲.__proto__是一個非標準的屬性,而且實例出的對象繼承原型對象的方法,所以直接可以寫成:obj.constructor.name

3.6 原型繼承

原型繼承:每一個構造函數都有prototype原型屬性,通過構造函數創建出來的對象都繼承自該原型屬性。所以可以通過更改構造函數的原型屬性來實現繼承。

繼承的方式有多種,可以一個對象繼承另一個對象,也可以通過原型繼承的方式進行繼承。

1、簡單混入繼承

直接遍歷一個對象,將所有的屬性和方法加到另一對象上。
var animal = {
    name:"Animal",
    sex:"male",
    age:5,
    bark:function(){
        console.log("Animal bark");
    }
};

var dog = {};

for (var k in animal){
    dog[k]= animal[k];
}

console.log(dog);  // 打印的對象與animal一模一樣

缺點:只能一個對象繼承自另一個對象,代碼複用太低了。

2、混入式原型繼承

混入式原型繼承其實與上面的方法類似,只不過是將遍歷的對象添加到構造函數的原型上。
var obj={
     name:'zs',
     age:19,
     sex:'male'
 }

function Person(){
    this.weight=50;
}

for(var k in obj){
    // 將obj裏面的所有屬性添加到 構造函數 Person 的原型中
    Person.prototype[k] = obj[k];
}

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // 'zs'
console.log(p2.age);   // 19
console.log(p3.sex);   // 'male'

面向對象思想封裝一個原型繼承

我們可以利用面向對象的思想,將面向過程進行封裝。
function Dog(){
    this.type = 'yellow Dog';
}

// 給構造函數 Dog 添加一個方法 extend
Dog.prototype.extend = function(obj){
    // 使用混入式原型繼承,給 Dog 構造函數的原型繼承 obj 的屬性和方法
     for (var k in obj){
        this[k]=obj[k];
    }
}

// 調用 extend 方法
Dog.prototype.extend({
    name:"二哈",
    age:"1.5",
    sex:"公",
    bark:function(){
        console.log('汪汪汪');
    }
});

3、替換式原型繼承

替換式原型繼承,在上面已經舉過例子了,其實就是將一個構造函數的原型對象替換成另一個對象。
function Person(){
    this.weight=50;
}

var obj={
    name:'zs',
    age:19,
    sex:'male'
}
// 將一個構造函數的原型對象替換成另一個對象
Person.prototype = obj;

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // 'zs'
console.log(p2.age);   // 19
console.log(p3.sex);   // 'male'

之前我們就說過,這樣做會產生一個問題,就是替換的對象會重新開闢一個新的空間。

替換式原型繼承時的bug

替換原型對象的方式會導致原型的constructor的丟失,constructor屬性是默認原型對象指向構造函數的,就算是替換了默認原型對象,這個屬性依舊是默認原型對象指向構造函數的,所以新的原型對象是沒有這個屬性的。

image

解決方法:手動關聯一個constructor屬性

function Person() {
    this.weight = 50;
}

var obj = {
    name: 'zs',
    age: 19,
    sex: 'male'
}
// 在替換原型對象函數之前 給需要替換的對象添加一個 constructor 屬性 指向原本的構造函數
obj.constructor = Person;

// 將一個構造函數的原型對象替換成另一個對象
Person.prototype = obj;

var p1 = new Person();

console.log(p1.__proto__.constructor === Person);  // true

4、Object.create()方法實現原型繼承

當我們想把對象1作爲對象2的原型的時候,就可以實現對象2繼承對象1。前面我們瞭解了一個屬性:__proto__,實例出來的對象可以通過這個屬性訪問到它的原型,但是這個屬性只適合開發調試時使用,並不能直接去替換原型對象。所以這裏介紹一個新的方法:Object.create()

語法: var obj1 = Object.create(原型對象);

示例代碼: 讓空對象obj1繼承對象obj的屬性和方法

var obj = {
    name : '蓋倫',
    age : 25,
    skill : function(){
        console.log('大寶劍');
    }
}

// 這個方法會幫我們創建一個原型是 obj 的對象
var obj1 = Object.create(obj);

console.log(obj1.name);     // "蓋倫"
obj1.skill();               // "大寶劍"

兼容性:

由於這個屬性是ECMAScript5的時候提出來的,所以存在兼容性問題。

利用瀏覽器的能力檢測,如果存在Object.create則使用,如果不存在的話,就創建構造函數來實現原型繼承。

// 封裝一個能力檢測函數
function create(obj){
    // 判斷,如果瀏覽器有 Object.create 方法的時候
    if(Object.create){
        return Object.create(obj);
    }else{
        // 創建構造函數 Fun
        function Fun(){};
        Fun.prototype = obj; 
        return new Fun();
    }
}

var hero = {
    name: '蓋倫',
    age: 25,
    skill: function () {
        console.log('大寶劍');
    }
}

var hero1 = create(hero);
console.log(hero1.name);    // "蓋倫"
console.log(hero1.__proto__ == hero);   // true

4.原型鏈

對象有原型,原型本身又是一個對象,所以原型也有原型,這樣就會形成一個鏈式結構的原型鏈。

4.1 什麼是原型鏈

示例代碼: 原型繼承練習

// 創建一個 Animal 構造函數
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log('蜂蜜蜂蜜');
    }
}

// 實例化一個 animal 對象
var animal = new Animal();

// 創建一個 Preson 構造函數
function Person() {
    this.name = 'zs';
    this.tool = function() {
        console.log('菜刀');
    }
}

// 讓 Person 繼承 animal (替換原型對象)
Person.prototype = animal;

// 實例化一個 p 對象 
var p = new Person();

// 創建一個 Student 構造函數
function Student() {
    this.score = 100;
    this.clickCode = function() {
        console.log('啪啪啪');
    }
}

// 讓 Student 繼承 p (替換原型對象)
Student.prototype = p;

//實例化一個 student 對象
var student = new Student();


console.log(student);           // 打印 {score:100,clickCode:fn}

// 因爲是一級級繼承下來的 所以最上層的 Animate 裏的屬性也是被繼承的
console.log(student.weight);    // 50
student.eat();         // 蜂蜜蜂蜜
student.tool();        // 菜刀

如圖所示:

我們將上面的案例通過畫圖的方式展現出來後就一目瞭然了,實例對象animal直接替換了構造函數Person的原型,以此類推,這樣就會形成一個鏈式結構的原型鏈。

image

完整的原型鏈

結合上圖,我們發現,最初的構造函數Animal創建的同時,會創建出一個原型,此時的原型是一個空的對象。結合原型鏈的概念:“原型本身又是一個對象,所以原型也有原型”,那麼這個空對象往上還能找出它的原型或者構造函數嗎?

我們如何創建一個空對象? 1、字面量:{};2、構造函數:new Object()。我們可以簡單的理解爲,這個空的對象就是,構造函數Object的實例對象。所以,這個空對象往上面找是能找到它的原型和構造函數的。

// 創建一個 Animal 構造函數
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log('蜂蜜蜂蜜');
    }
}

// 實例化一個 animal 對象
var animal = new Animal();

console.log(animal.__proto__);      // {}
console.log(animal.__proto__.__proto__);  // {}
console.log(animal.__proto__.__proto__.constructor);  // function Object(){}
console.log(animal.__proto__.__proto__.__proto__);  // null

如圖所示:

image

4.2 原型鏈的拓展

1、描述出數組“[]”的原型鏈結構

// 創建一個數組
var arr = new Array();

// 我們可以看到這個數組是構造函數 Array 的實例對象,所以他的原型應該是:
console.log(Array.prototype);   // 打印出來還是一個空數組

// 我們可以繼續往上找 
console.log(Array.prototype.__proto__);  // 空對象

// 繼續
console.log(Array.prototype.__proto__.__proto__)  // null

如圖所示:

image

2、擴展內置對象

js原有的內置對象,添加新的功能。

注意:這裏不能直接給內置對象的原型添加方法,因爲在開發的時候,大家都會使用到這些內置對象,假如大家都是給內置對象的原型添加方法,就會出現問題。

錯誤的做法:

// 第一個開發人員給 Array 原型添加了一個 say 方法
Array.prototype.say = function(){
    console.log('哈哈哈');
}

// 第二個開發人員也給 Array 原型添加了一個 say 方法
Array.prototype.say = function(){
    console.log('啪啪啪');
}

var arr = new Array();

arr.say();  // 打印 “啪啪啪”  前面寫的會被覆蓋

爲了避免出現這樣的問題,只需自己定義一個構造函數,並且讓這個構造函數繼承數組的方法即可,再去添加新的方法。

// 創建一個數組對象 這個數組對象繼承了所有數組中的方法
var arr = new Array();

// 創建一個屬於自己的構造函數
function MyArray(){}

// 只需要將自己創建的構造函數的原型替換成 數組對象,就能繼承數組的所有方法
MyArray.prototype = arr;

// 現在可以單獨的給自己創建的構造函數的原型添加自己的方法
MyArray.prototype.say = function(){
    console.log('這是我自己添加的say方法');
}

var arr1 = new MyArray();

arr1.push(1);   // 創建的 arr1 對象可以使用數組的方法
arr1.say();     // 也可以使用自己添加的方法  打印“這是我自己添加的say方法”
console.log(arr1);  // [1]

4.3 屬性的搜索原則

當通過對象名.屬性名獲取屬性時,會遵循以下屬性搜索的原則:
  • 1-首先去對象自身屬性中找,如果找到直接使用,
  • 2-如果沒找到,去自己的原型中找,如果找到直接使用,
  • 3-如果沒找到,去原型的原型中繼續找,找到直接使用,
  • 4-如果沒有會沿着原型不斷向上查找,直到找到null爲止。

5.Object.prototype成員介紹

我們可以看到所有的原型最終都會繼承Object的原型:Object.prototype

打印看看Object的原型裏面有什麼:

// Object的原型
console.log(Object.prototype)

如圖所示:

image

我們可以看到Object的原型裏有很多方法,下面就來介紹下這些方法的作用。

5.1 constructor 屬性

指向了和原型相關的構造函數

5.2 hasOwnProperty 方法

判斷對象自身是否擁有某個屬性,返回值:布爾類型

示例代碼:

function Hero() {
    this.name = '蓋倫';
    this.age = '25';
    this.skill = function () {
        console.log('蓋倫使用了大寶劍');
    }
}

var hero = new Hero();
console.log(hero.name); // '蓋倫'
hero.skill();           // '蓋倫使用了大寶劍'

console.log(hero.hasOwnProperty("name"));       // true
console.log(hero.hasOwnProperty("age"));        // true
console.log(hero.hasOwnProperty("skill"));      // true
console.log(hero.hasOwnProperty("toString"));   // false toString是在原型鏈當中的方法,並不是這裏對象的方法

console.log('toString' in hero); // true in方法 判斷對象自身或者原型鏈中是否有某個屬性

5.3 isPrototypeOf 方法

對象1.isPrototypeOf(對象2),判斷對象1是否是對象2的原型,或者對象1是否是對象2原型鏈上的原型。

示例代碼:

var obj = {
    age: 18
}
var obj1 = {};

// 創建一個構造函數
function Hero() {
    this.name = '蓋倫';
}

// 將這個構造函數的原型替換成 obj
Hero.prototype = obj;

// 實例化一個 hero 對象
var hero = new Hero();

console.log(obj.isPrototypeOf(hero));   // true  判斷 obj 是否是 hero 的原型
console.log(obj1.isPrototypeOf(hero));  // false  判斷 obj1 是否是 hero 的原型
console.log(Object.prototype.isPrototypeOf(hero));  // true  判斷 Object.prototype 是否是 hero 的原型
// 注意 這裏的 Object.prototype 是原型鏈上最上層的原型對象

5.4 propertyIsEnumerable 方法

對象.propertyIsEnumerable('屬性或方法名'),判斷一個對象是否有該屬性,並且這個屬性可以被for-in遍歷,返回值:布爾類型

示例代碼:

// 創建一個構造函數
function Hero (){
    this.name = '蓋倫';
    this.age = 25;
    this.skill = function(){
        console.log('蓋倫使用了大寶劍');
    }
}

// 創建一個對象
var hero = new Hero();

// for-in 遍歷這個對象 我們可以看到分別打印了哪些屬性和方法
for(var k in hero){
    console.log(k + '—' + hero[k]); // "name-蓋倫" "age-25" "skill-fn()"
}

// 判斷一個對象是否有該屬性,並且這個屬性可以被 for-in 遍歷
console.log(hero.propertyIsEnumerable('name'));     // true
console.log(hero.propertyIsEnumerable('age'));      // true
console.log(hero.propertyIsEnumerable('test'));     // false

5.5 toString 和 toLocalString 方法

兩種方法都是將對象轉成字符串的,只不過toLocalString是按照本地格式進行轉換。

示例代碼:

// 舉個例子,時間的格式可以分爲世界時間的格式和電腦本地的時間格式
var date = new Date();

// 直接將創建的時間對象轉換成字符串
console.log(date.toString());

// 將創建的時間對象按照本地格式進行轉換
console.log(date.toLocaleString());

效果圖:

image

5.6 valueOf 方法

返回指定對象的原始值。

MDN官方文檔

6.靜態方法和實例方法

靜態方法和實例方法這兩個概念其實也是從面相對象的編程語言中引入的,對應到JavaScript中的理解爲:

靜態方法: 由構造函數調用的

js中,我們知道有個Math構造函數,他有一個Math.abs()的方法,這個方法由構造函數調用,所以就是靜態方法。
Math.abs();

實例方法: 由構造函數創建出來的對象調用的

var arr = new Array();

// 由構造函數 Array 實例化出來的對象 arr 調用的 push 方法,叫做實例方法
arr.push(1);

示例代碼:

function Hero(){
    this.name='亞索';
    this.say=function(){
        console.log('哈撒ki');
    }
}
Hero.prototype.skill=function(){
    console.log('吹風');
}

// 直接給構造函數添加一個 run 方法(函數也是對象,可以直接給它加個方法)
Hero.run=function(){
    console.log('死亡如風,常伴吾身');
}

var hero = new Hero();

hero.say();
hero.skill();   //實例方法

Hero.run();     //靜態方法

如果這個方法是對象所有的,用實例方法。一般的工具函數,用靜態方法,直接給構造函數添加方法,不需要實例化,通過構造函數名直接使用即可;

7.作用域

“域”,表示的是一個範圍,“作用域”就是作用範圍。作用域說明的是一個變量可以在什麼地方被使用,什麼地方不能被使用。

7.1 塊級作用域

ES5ES5之前,js中是沒有塊級作用域的。
{
    var num = 123;
    {
        console.log( num ); // 123
    }
}
console.log( num ); // 123

上面這段代碼在JavaScript中是不會報錯的,但是在其他的編程語言中(C#、C、JAVA)會報錯。這是因爲,在JavaScript中沒有塊級作用域,使用{}標記出來的代碼塊中聲明的變量num,是可以被{}外面訪問到的。但是在其他的編程語言中,有塊級作用域,那麼{}中聲明的變量num,是不能在代碼塊外部訪問的,所以報錯。

注意:會計作用域只在在ES5ES5之前不起作用,但是在ES6開始,js中是存在塊級作用域的。

7.2 詞法作用域

詞法( 代碼 )作用域,就是代碼在編寫過程中體現出來的作用範圍。代碼一旦寫好,不用執行,作用範圍就已經確定好了,這個就是所謂詞法作用域。

js中詞法作用域規則:

  • 函數允許訪問函數外的數據;
  • 整個代碼結構中只有函數可以限定作用域;
  • 作用域規則首先使用提升規則分析;
  • 如果當前作用規則中有名字了,就不考慮外面的名字。

作用域練習:

第一題

var num=250;

function test(){
    // 會現在函數內部查找有沒有這個num變量,有的話調用,沒有的話會去全局中查找,有就返回,沒有就返回undefined
    console.log(num);  // 打印 250
}

function test1(){
   var num=222;
   test();
}

test1();  

第二題

if(false){
    var num = 123;
}

console.log(num); // undefined 
// {}是沒有作用域的 但是有判斷條件,var num會提升到判斷語句外部 所以不會報錯 打印的是undefined

第三題

var num = 123;
function foo() {
    var num = 456;
    function func() {
        console.log( num );
    }
    func();
}
foo();  // 456
// 調用foo時,在函數內部調用了func,打印num的時候,會先在func中查找num  沒有的時候會去外層作用域找,找到即返回,找不到即再往上找。

第四題

var num1 = 123;
function foo1() {
   var num1 = 456;
   function foo2() {
       num1 = 789;
       function foo3 () {
           console.log( num1 );  // 789  自己的函數作用域中沒有就一層層往上找
       }
       foo3();
   }
   foo2();
}
foo1();
console.log( num1 ); // 123 

7.3 變量提升(預解析)

JavaScript是解釋型的語言,但是它並不是真的在運行的時候逐句的往下解析執行。

我們來看下面這個例子:

func();

function func(){
    alert("函數被調用了");
}

在上面這段代碼中,函數func的調用是在其聲明之前,如果說JavaScript代碼真的是逐句的解析執行,那麼在第一句調用的時候就會出錯,然而事實並非如此,上面的代碼可以正常執行,並且alert出來"函數被調用了"。

所以,可以得出結論,JavaScript並非僅在運行時簡簡單單的逐句解析執行!

JavaScript預解析

JavaScript引擎在對JavaScript代碼進行解釋執行之前,會對JavaScript代碼進行預解析,在預解析階段,會將以關鍵字varfunction開頭的語句塊提前進行處理。

關鍵問題是怎麼處理呢?

  • 當變量和函數的聲明處在作用域比較靠後的位置的時候,變量和函數的聲明會被提升到當前作用域的開頭。

示例代碼:函數名提升

  • 正常函數書寫方式
function func(){
    alert("函數被調用了");
}
func();
  • 預解析之後,函數名提升
func();
function func(){
    alert("函數被調用了");
}

示例代碼:變量名提升

  • 正常變量書寫方式
alert(a);  // undefined  
var a = 123;
// 由於JavaScript的預解析機制,上面這段代碼,alert出來的值是undefined,
// 如果沒有預解析,代碼應該會直接報錯a is not defined,而不是輸出值。
  • 不是說要提前的嗎?那不是應該alert出來123,爲什麼是undefined?
// 變量的時候 提升的只是變量聲明的提升,並不包括賦值
var a;      // 這裏是聲明
alert(a);   // 變量聲明之後並未有初始化和賦值操作,所以這裏是 undefined
a = 123;    // 這裏是賦值

注意:特殊情況

1、函數不能被提升的情況

  • 函數表達式創建的函數不會提升
test();   // 報錯 "test is not a function"
var test = function(){
    console.log(123);
}
  • new Function創建的函數也不會被提升
test();   // 報錯 "test is not a function"
var test = new Function(){
    console.log(123);
}

2、出現同名函數

test();  // 打印 '好走的都是下坡路'

// 兩個函數重名,這兩個函數都會被提升,但是後面的函數會覆蓋掉前面的函數
function test(){
   console.log('衆裏尋她千百度,他正在自助烤肉....');
}

function test(){
   console.log('好走的都是下坡路');
}

3、函數名與變量名同名

// 如果函數和變量重名,只會提升函數,變量不會被提升
console.log(test);  // 打印這個test函數

function test(){
   console.log('我是test');
}
var test=200;

再看一種情況:

var num = 1;
function num () {
    console.log(num); // 報錯 “num is not a function”
}
num();

直接上預解析後的代碼:

function num(){
    console.log(num);
}

num = 1;
num();

4、條件式的函數聲明

// 如果是條件式的函數申明, 這個函數不會被預解析
test();  // test is not a function
if(true){
    function test(){
        console.log('只是在人羣中多看了我一眼,再也忘不掉我容顏...');
    }
}

預解析是分作用域的

聲明提升並不是將所有的聲明都提升到window 對象下面,提升原則是提升到變量運行的當前作用域中去。

示例代碼:

function showMsg(){
    var msg = 'This is message';
}
alert(msg); // 報錯“Uncaught ReferenceError: msg is not defined”

預解析之後:

function showMsg(){
    var msg;    // 因爲函數本身就會產生一個作用域,所以變量聲明在提升的時候,只會提升在當前作用域下最前面
    msg = 'This is message';
}
alert(msg); // 報錯“Uncaught ReferenceError: msg is not defined”

預解析是分段的

分段,其實就分script標籤的
<script>
func(); // 輸出 AA2;
function func(){
    console.log('AA1');
}

function func(){
    console.log('AA2');
}
</script>

<script>
function func(){
    console.log('AA3');
}
</script>

在上面代碼中,第一個script標籤中的兩個func進行了提升,第二個func覆蓋了第一個func,但是第二個script標籤中的func並沒有覆蓋上面的第二個func。所以說預解析是分段的。

tip: 但是要注意,分段只是單純的針對函數,變量並不會分段預解析。

函數預解析的時候是分段的,但是執行的時候不分段

<script>
    //變量預解析是分段的 ,但是函數的執行是不分段
    var num1=100;

    // test3();  報錯,函數預解析的時候分段,執行的時候纔不分段
    function test1(){
        console.log('我是test1');
    }

    function test2(){
        console.log('我是test2');
    }
</script>

<script>
    var num2=200;
    function test3(){
        console.log('test3');
    }

    test1();   // 打印 '我是test1' 函數執行的時候不分段
    console.log(num1); // 100
</script>

7.4 作用域鏈

什麼是作用域鏈?

只有函數可以製造作用域結構,那麼只要是代碼,就至少有一個作用域, 即全局作用域。

凡是代碼中有函數,那麼這個函數就構成另一個作用域。如果函數中還有函數,那麼在這個作用域中就又可以誕生一個作用域。將這樣的所有的作用域列出來,可以有一個結構: 函數內指向函數外的鏈式結構。就稱作作用域鏈。

例如:

function f1() {
    function f2() {
    }
}

var num = 456;
function f3() {
    function f4() {    
    }
}

示例代碼:

var num=200;
function test(){
    var num=100;
    function test1(){
        var num=50;
        function test2(){
            console.log(num);
        }
        test2();
    }
    test1();
}

test();   // 打印 “50”

如圖所示:

image

繪製作用域鏈的步驟:

  • 看整個全局是一條鏈, 即頂級鏈, 記爲0級鏈
  • 看全局作用域中, 有什麼變量和函數聲明, 就以方格的形式繪製到0級練上
  • 再找函數, 只有函數可以限制作用域, 因此從函數中引入新鏈, 標記爲1級鏈
  • 然後在每一個1級鏈中再次往復剛纔的行爲

變量的訪問規則:

  • 首先看變量在第幾條鏈上, 在該鏈上看是否有變量的定義與賦值, 如果有直接使用
  • 如果沒有到上一級鏈上找( n - 1 級鏈 ), 如果有直接用, 停止繼續查找.
  • 如果還沒有再次往上剛找... 直到全局鏈( 0 級 ), 還沒有就是 is not defined
  • 注意,同級的鏈不可混合查找

來點案例練練手

第一題:

function foo() {
    var num = 123;
    console.log(num); //123
}
foo();
console.log(num); // 報錯

第二題:

var scope = "global";
function foo() {
    console.log(scope); //  undefined
    var scope = "local";
    console.log(scope); // 'local'
}
foo();

// 預解析之後
// var scope = "global";
// function foo() {
//   var scope;
//   console.log(scope); // undefined
//   scope = "local";
//   console.log(scope); // local
// }

第三題:

if("a" in window){
   var a = 10;
}
console.log(a); // 10

// 預解析之後
// var a;
// if("a" in window){
//    a = 10;        // 判斷語句不產生作用域
// }
// console.log(a); // 10

第四題:

if(!"a" in window){
   var a = 10;
}
console.log(a); // undefined

// 預解析之後
// var a;
// if(!"a" in window){
//    a = 10;        // 判斷語句不產生作用域
// }
// console.log(a); // undefined

第五題

// console.log(num); 報錯 雖然num是全局變量 但是不會提升
function test(){
   num = 100;  
}

test();

console.log(num);   // 100

第六題

var foo = 1;
function bar() {
   if(!foo) {
       var foo = 10;
   }
   console.log(foo); // 10
}
bar();

// 預解析之後
// var foo=1;
// function bar(){
//    var foo;
//    if(!foo){
//        foo=10;
//    }
//    console.log(foo); // 10
// }
// bar();

8.Function

Function是函數的構造函數,你可能會有點蒙圈,沒錯,在js中函數與普通的對象一樣,也是一個對象類型,只不過函數是js中的“一等公民”。

這裏的Function類似於ArrayObject

8.1 創建函數的幾種方式

1、函數字面量(直接聲明函數)創建方式

function test(){   
    // 函數體
}   // 類似於對象字面量創建方式:{}

2、函數表達式

var test = function(){
    // 函數體
}

3、Function構造函數創建

// 構造函數創建一個空的函數
var fn = new Function();
fn1();  // 調用函數

函數擴展名

有沒有一種可能,函數表達式聲明函數時,function 也跟着一個函數名,如:var fn = function fn1(){}? 答案是可以的,不過fn1只能在函數內部使用,並不能在外部調用。
var fn = function fn1(a,b,c,d){
    console.log('當前函數被調用了');
    // 但是,fn1可以在函數的內部使用
    console.log(fn1.name);
    console.log(fn1.length);
    // fn1();  注意,這樣調用會引起遞歸!!!  下面我們會講到什麼是遞歸。
}
// fn1();   // 報錯,fn1是不能在函數外部調用的
fn();   // "當前函數被調用了"

// 函數內部使用時打印:
// "當前函數被調用了"
// console.log(fn1.name); => "fn1"
// console.log(fn1.length); => 4

8.2 Function 構造函數創建函數

上面我們知道了如何通過Function構造函數創建一個空的函數,這裏我們對它的傳參詳細的說明下。

1、不傳參數時

// 不傳參數時,創建的是一個空的函數
var fn1 = new Function();
fn1();  // 調用函數

2、只傳一個參數

// 只傳一個參數的時候,這個參數就是函數體
// 語法:var fn = new Function(函數體);
var fn2 = new Function('console.log(2+5)');
f2();   // 7

3、傳多個參數

// 傳多個參數的時候,最後一個參數爲函數體,前面的參數都是函數的形參名
// 語法:var fn = new Function(arg1,arg2,arg3.....argn,metthodBody);
var fn3 = new Function('num1','num2','console.log(num1+num2)');
f3(5,2);   // 7

8.3 Function 的使用

1、用Function創建函數的方式封裝一個計算m - n之間所有數字的和的函數

//求 m-n之間所有數字的和
//var sum=0;
//for (var i = m; i <=n; i++) {
//  sum+=i;
//}
var fn = new Function('m','n','var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);');
fn(1,100);  // 5050

函數體參數過長問題:

函數體過長時,可讀性很差,所以介紹解決方法:

1)字符串拼接符“+

var fn = new Function(
    'm',
    'n',
    'var sum=0;'+
    'for (var i = m; i <=n; i++) {'+
        'sum += i;'+
    '}'+
    'console.log(sum);'
    );
fn(1,100);  // 5050

2)ES6中新語法“ ` ”,(在esc鍵下面)

表示可換行字符串的界定符,之前我們用的是單引號或者雙引號來表示一個字符串字面量,在ES6中可以用反引號來表示該字符串可換行。
new Function(
    'm',
    'n',
    `var sum=0;
    for (var i = m; i <=n; i++) {
        sum+=i;
    }
    console.log(sum);`
);

3)模板方式

<!-- 新建一個模板 -->
<script type="text/template" id="tmp">
    var sum=0;
    for (var i = m; i <=n; i++) {
        sum += i;
    }
    console.log(sum);
</script>

<script>
    // 獲取模板內的內容
    var methodBody = document.querySelector('#tmp').innerHTML;
    console.log(methodBody);
    var fn = new Function('m','n',methodBody);
    
    fn(2,6);  // 20
</script>

2、eval 函數

eval函數可以直接將把字符串的內容,作爲js代碼執行,前提是字符串代碼符合js代碼規範。這裏主要是用作跟Function傳參比較。

evalFunction 的區別:

  • Function();中,方法體是字符串,必須調用這個函數才能執行
  • eval(); 可以直接執行字符串中的js代碼

存在的問題:

  • 性能問題
因爲eval裏面的代碼是直接執行的,所以當在裏面定義一個變量的時候,這個變量是不會預解析的,所以會影響性能。
// eval 裏面的代碼可以直接執行,所以下面的打印的 num 可以訪問到它
// 但是這裏定義的 num 是沒有預解析的,所以變量名不會提升,從而性能可能會變慢
eval('var num = 123;');
console.log(num);   // 123
  • 安全問題
主要的安全問題是可能會被利用做XSS攻擊(跨站腳本攻擊(Cross Site Scripting)),eval也存在一個安全問題,因爲它可以執行傳給它的任何字符串,所以永遠不要傳入字符串或者來歷不明和不受信任源的參數。

示例代碼: 實現一個簡單的計算器

<!-- html 部分 -->
<input type="text" class="num1">
<select class="operator">
    <option value="+">+</option>
    <option value="-">-</option>
    <option value="*">*</option>
    <option value="/">/</option>
</select>
<input type="text" class="num2">
<button>=</button>
<input type="text" class="result">

<!-- js 部分 -->
<script>
    document.querySelector('button').onclick=function(){
        var num1 = document.querySelector('.num1').value;
        var num2 = document.querySelector('.num2').value;
        var operator = document.querySelector('.operator').value;
        
        // result其實最終獲得的就是 num1 + operator + num2的字符串  但是他能夠直接執行並計算
        var result = eval(num1 + operator + num2);          //計算
        document.querySelector('.result').value = result;   //顯示
    }
</script>

效果圖:

image

8.4 Function 的原型鏈結構

7.2章節中我們知道函數也還可以通過構造函數的方式創建出來,既然可以通過構造函數的方式創建,那麼函數本身也是有原型對象的。

示例代碼:

// 通過Function構造函數創建一個函數test
var test = new Function();
// 既然是通過構造函數創建的,那麼這個函數就有指向的原型
console.log(test.__proto__);  // 打印出來的原型是一個空的函數
console.log(test.__proto__.__proto__);  // 空的函數再往上找原型是一個空的對象
console.log(test.__proto__.__proto__.__proto__);    // 再往上找就是null了

// 函數原型鏈: test() ---> Function.prototype ---> Object.prototype ---> null

如圖所示:

image

通過上圖,可以直觀的看出,函數也是有原型的。那一個完整的原型鏈究竟是什麼樣子的呢?下面我們一起做個總結。

8.5 完整的原型鏈

繪製完整原型鏈的步驟:

  • 1、先將一個對象的原型畫出來
  • 2、再把對象的原型的原型鏈畫出來 ,到null結束
  • 3、把對象的構造函數的原型鏈畫出來
  • 4、把FunctionObject的原型關係給畫出來

示例代碼:

// 創建一個構造函數
function Person(){
    this.name = 'Levi';
    this.age = 18;
}

// 實例化一個對象
var p = new Person();

如圖所示:

image

總結:

  • Function構造函數的原型,在Object的原型鏈上;
  • Object構造函數的原型,在Function的原型鏈上;

9.arguments對象

在每一個函數調用的過程中, 函數代碼體內有一個默認的對象arguments, 它存儲着實際傳入的所有參數。

示例代碼:

// 封裝一個加法函數
function add(num1,num2){
    console.log(num1+num2);
}

add(1);     // NaN
add(1,2);   // 3
add(1,2,3); // 3

在調用函數時,實參和形參的個數可以不一樣,但是沒有意義。

在函數內部有個arguments對象(注意:是在函數內部),arguments是一個僞數組對象。它表示在函數調用的過程中傳入的所有參數(實參)的集合。在函數調用過程中不規定參數的個數與類型,可以使得函數調用變得非常靈活性。
function add(num1,num2){
    console.log(arguments); // 打印的是一個僞數組
}
add(1,2,3,4); 

image

  • length:表示的是實參的個數;
  • callee:指向的就是arguments對象所在的函數;

示例代碼:

封裝一個求最大值的函數,因爲不知道需要傳進多少實參,所以直接用僞數組arguments獲取調用的實參
function max(){
    // 假使實參的第一個數字最大
    var maxNum = arguments[0];
    // 循環這個僞數組
    for(var i = 0; i < arguments.length; i++){
        if(maxNUm < arguments[i]){
            maxNUm = arguments[i];
        }
        return maxNum;
    }
    
}

// 調用
console.log(max(1,9,12,8,22,5));   // 22

10. 函數的四種調用模式

四種調用模式分別是:“函數調用模式”、“方法調用模式”、“構造器調用模式”、“上下文調用模式”。

其實就是分析this是誰的問題。只看函數是怎麼被調用的,而不管函數是怎麼來的。

  • 分析this屬於哪個函數;
  • 分析這個函數是以什麼方式調用的;

什麼是函數? 什麼是方法?

如果一個函數是掛載到一個對象中,那麼就把這個函數稱爲方法

如果一個函數直接放在全局中,由Window對象調用,那麼他就是一個函數。

// 函數
function fn() {}
var f = function() {};
fn();
f();

// 方法
var obj = {
  say: function() {}
};
obj.say();

fnf都是函數,say是一個方法

10.1 函數模式

函數模式其實就是函數調用模式,this是指向全局對象window的。
this -> window

示例代碼:

// 函數調用模式:
// 創建的全局變量相當於window的屬性
var num = 999;
var fn = function () {
  console.log(this);  // this 指向的是 window 對象
  console.log(this.num); // 999
};
fn();

10.2 方法模式

方法模式其實就是方法調用模式,this是指向調用方法的對象。
this -> 調用方法的對象

示例代碼:

// this指向的是obj 
var age = 38;
var obj = {
  age: 18,
  getAge: function () {
    console.log(this);  // this指向的是對象obj  {age:18,getAge:f()}
    console.log(this.age);  // 18
  } 
};
obj.getAge(); // getAge() 是對象 obj 的一個方法

10.3 構造器模式

構造器模式其實就是構造函數調用模式,this指向新創建出來的實例對象。
this -> 新創建出來的實例對象

示例代碼:

// this指向的是實例化出來的對象
function Person(name){
    this.name = name;
    console.log(this);
}
var p1 = new Person('Levi'); // Person {name: "Levi"}
var p2 = new Person('Ryan'); // Person {name: "Ryan"}

構造函數的返回值:

如果返回的是基本類型
function Person() {
    return 1;
}
var p1 = new Person();
console.log(p1);  // 打印Person {}

構造函數內有返回值,且是基本類型的時候,返回值會被忽略掉,返回的是實例出來的對象。

如果返回的是引用類型
function Person() {
  return {
    name: 'levi',
    age: 18
  };
}
var p1 = new Person();
console.log(p1); // 此時打印 Object {name: 'levi', age: 18}

構造函數內的返回值是一個引用類型的時候,返回的就是這個指定的引用類型。

10.4 上下文(借用方法)模式

上下文,即環境,用於指定方法內部的this,上下文調用模式中,this可以被隨意指定爲任意對象。

上下文模式有兩種方法,是由函數調用的:

  • 函數名.apply( ... );
  • 函數名.call( ... );

1、apply 方法

語法:

fn.apply(thisArg, array);

參數:

第一個參數:表示函數內部this的指向(或者:讓哪個對象來借用這個方法)
第二個參數:是一個數組(或者僞數組),數組中的每一項都將作爲被調用方法的參數

示例代碼:

// 沒有參數
function fn (){
    console.log(this.name);
}

var obj = {
    name : 'Levi丶'
}

// this 指向 obj,fn 借用obj方法裏面的 name 屬性
fn.apply(obj);  // 打印 'Levi丶'
// 有參數
function fn (num1, num2){
    console.log(num1 + num2);
}

var obj = {}

// this 指向 obj,數組中的數據是方法 fn 的參數
fn.apply(obj, [1 , 2]);  // 打印 3

注意:apply方法的第一個參數,必須是一個對象!如果傳入的參數不是一個對象,那麼這個方法內部會將其轉化爲一個包裝對象。

function fn() {
  console.log(this);
}

fn.apply(1); // 包裝對象
fn.apply('abc'); // 包裝對象
fn.apply(true); // 包裝對象

image

指向window的幾種方式:

function fn(){
    
}

fn.apply(window);
fn.apply();
fn.apply(null);
fn.apply(undefined);

具體應用:

  • 求數組中的最大數
// 以前的方法,假設第一項最大,然後與後面每一項比較,得到最大的項
var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];

var maxNum = arr[0];
for(var i = 1; i < arr.length; i++) {
  if(maxNum < arr[i]) {
    maxNum = arr[i];
  }
}
console.log(maxNum); // 777
// 利用 內置對象的 apply 的方法
var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];

// max 是內置對象 Math 求最大值的一個方法
var maxNum = Math.max.apply(null, arr);
console.log(maxNum); // 777
  • 將傳進的參數每一項之間用“-”連接
// 思考:參數個數是用戶隨機傳的,沒有具體的一個值,這時候就需要用到 arguments 的概念了
function fn (){
    // 數組原型中有一個join方法,他的接收的參數是一個字符串
    // join.apply的第一個參數指向 arguments 對象,第二個參數是jion方法需要的參數
    return Array.prototype.join.apply(arguments, ['-']);
}

var ret = fn('a', 'b', 'c', 'd', 'e');
console.log(ret); // 'a-b-c-d-e'

2、call 方法

call方法的作用於apply方法的作用相同,唯一不同的地方就是第二個參數不同。

語法:

fn.apply(thisArg, parm1,parm2,parm3,...);

參數:

第一個參數:表示函數內部this的指向(或者:讓哪個對象來借用這個方法)
第二個及後面的參數:不是之前數組的形式了,對應方法調用的每一個參數

示例代碼:

function fn(num1, num2, num3) {
  console.log(num1, num2, num3);
}

var obj = {};
fn.call(obj, [1, 3, 9], 0, 1); // [1, 3, 9] 0 1
fn.call(obj, [1, 3, 9]); // [1, 3, 9] undefined undefined

3、apply 和 call 的區別

兩者在功能上一模一樣,唯一的區別就是第二個參數傳遞的類型不一樣。

什麼時候用apply?什麼時候用call呢?

其實用哪個都可以,在參數少的情況下,我們可以使用call方法,但是如果參數是僞數組或者是數組的時候,call方法就不適用了,還需要將僞數組中的每一項取出來作爲方法的參數,此時apply更加實用。

10.5 面試題分析

面試題1:

var age = 38;
var obj = {
    age: 18,
    getAge: function() {
        function foo() {
            console.log(this.age); // 這裏的this屬於函數 foo;   打印 38
        }
        foo(); // foo函是Window對象調用的
    }
};
obj.getAge();

面試題2:

// 只看函數是怎麼被調用的,而不管函數是怎麼來的
var age = 38;
var obj = {
    age: 18,
    getAge: function() {
        alert(this.age); 
    }
};

var f = obj.getAge;
f(); // 函數是Window對象調用的,所以this指向Window對象。打印:38

面試題3:

var length = 10;
function fn(){
    console.log(this.length);
}
var obj = {
    length: 5,
    method: function (fn) {
        fn();   // window對象調用 打印 10
        arguments[0](); // 方法調用模式,是arguments對象調用的  
        // this指向arguments,所以arguments.length = 2; (arguments.length:實參的個數)所以打印 2
    }
};
obj.method(fn, 123);

面試題4:

怎麼使用call或者apply方法實現構造函數的複用呢?
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}
function Teacher(name, age, gender, workYear, subject) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.workYear = workYear;
  this.subject = subject;
}
function Student(name, age, gender, stuNo, score) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.stuNo = stuNo;
  this.score = score;
}

var tec = new Teacher('張老師', 32, 'male', '7年', '語文');
var stu = new Student('xiaowang', 18, 'male', 10001, 99);
console.log(tec); // Teacher {name: "張老師", age: 32, gender: "male", workYear: "7年", subject: "語文"}
console.log(stu); // Student {name: "xiaowang", age: 18, gender: "male", stuNo: 10001, score: 99}

上面的代碼中一個Teacher構造函數,一個Student構造函數,他們都有一些公共的屬性,跟Person構造函數裏面的屬性重複,我們能否使用call或者apply方法,簡化上面的代碼呢?

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}
function Teacher(name, age, gender, workYear, subject) {
  // 借用 Person 函數來給當前對象添加屬性
  Person.call(this, name, age, gender);  // 這裏的this指向的就是當前的Teacher構造函數
  this.workYear = workYear;
  this.subject = subject;
}
function Student(name, age, gender, stuNo, score) {
  Person.call(this, name, age, gender);  // 這裏的this指向的就是當前的Student構造函數
  this.stuNo = stuNo;
  this.score = score;
}

var tec = new Teacher('張老師', 32, 'male', '7年', '語文');
var stu = new Student('xiaowang', 18, 'male', 10001, 99);
console.log(tec); // Teacher {name: "張老師", age: 32, gender: "male", workYear: "7年", subject: "語文"}
console.log(stu); // Student {name: "xiaowang", age: 18, gender: "male", stuNo: 10001, score: 99}

11.遞歸

11.1 什麼是遞歸

什麼是遞歸?遞歸就是函數直接自己調用自己或者間接的調用自己。

舉個例子:

  • 函數直接調用自己
function fn(){
    fn();
}
fn();
  • 函數間接調用自己
function fn1(){
    fn2();
}


function fn2(){
    fn1();
}

遞歸示例代碼:

function fn (){
    console.log('從前有座山,');
    console.log('山裏有座廟,');
    console.log('廟裏有個老和尚,');
    console.log('老和尚給小和尚講,');
    fn();
}
fn();  // 產生遞歸,無限打印上面的內容

這樣做會進入到無限的死循環當中。

11.2 化歸思想

化歸思想是將一個問題由難化易,由繁化簡,由複雜化簡單的過程稱爲化歸,它是轉化和歸結的簡稱。

合理使用遞歸的注意點:

  • 函數調用了自身
  • 必須有結束遞歸的條件,這樣程序就不會一直運行下去了

示例代碼: 求前n項的和

  • 求前n項的和其實就是:1 + 2 + 3 +...+ n
  • 尋找遞推關係,就是nn-1, 或n-2之間的關係:sum(n) == n + sum(n - 1)
  • 加上結束的遞歸條件,不然會一直運行下去。
function sum(n){
    if(n == 1) return 1;  // 遞歸結束條件 
    return n + sum(n - 1);
}

sum(100); // 打印 5050

遞推關係:

image

11.3 遞歸練習

1、求n的階乘:

思路:

  • f(n) = n * f(n - 1);
  • f(n - 1) = (n - 1) * f(n - 2);

示例代碼:

function product(n){
    if(n == 1) {
        return 1;
    }
    return n * product(n-1);
}
console.log(product(5));  // 打印 120

2、求m的n次冪:

思路:

  • f(m,n) = m * f(m,n-1);

示例代碼:

function pow(m,n){
    if(n==1){
        return m;
    }
    return m * pow(m,n-1);
}

console.log(pow(2, 10));  // 打印 1024

3、斐波那契數列

思路:什麼是斐波那契數列?

1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55,...

數字從第三項開始,每一項都等於前兩項的和。可得出公式:fn = f(n-1) + f(n-2),結束遞歸的條件:當n <= 2時,fn = 1

示例代碼:

function fib(n){
    if(n<=2) return 1;  // 結束遞歸的條件
    return fib(n-1) + fib(n-2);
}
console.log(fib(5)); // 5
console.log(fib(10)); // 55
console.log(fib(25)); // 75025   // 數值太大會影響性能問題

存在問題:

數值太大時會影響性能,怎麼影響的呢?
function fib(n){
    if(n<=2) return 1;
    return fib(n-1) + fib(n-2);
    // 當我們在計算一個值的時候,都是通過計算他的fib(n-1) 跟 fib(n-2)項之後再去進行相加,得到最終的值
    // 這時候就需要調用兩次這個函數,在計算fib(n-1)的時候,其實也是調用了兩次這個函數,得出fib(n-1)的值
}
// 記錄執行的次數
var count=0;
function  fib(n){
    count++;
    if(n<=2) return 1;
    return fib(n-1)+fib(n-2);
}

console.log(fib(5));  // 5
console.log(count);   // 9  求第五項的時候就計算了9次

//console.log(fib(20));  // 6765
//console.log(count);   // 13529  求第20項的時候就計算了13529次

image

這個問題在下面講閉包的時候解決。

4.獲取頁面所有的元素,並加上邊框

頁面結構:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div>
        <p>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
        </p>
        <p>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
        </p>
    </div>    
    <div>
        <p>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
        </p>
        <p>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
            <span>我是span標籤</span>
        </p>
    </div>
</body>
</html>

結構圖:

image

js代碼:

// 封裝一個方法,獲取到所有的標籤,並且給這些標籤加上邊框
function childrenTag(ele){
    var eleArr = []; // 用於存放所有的獲取到的標籤
    var elements = ele.children; // 獲取傳入元素下的直接子元素 (僞數組)
    for(var i = 0; i < elements.length; i++){
        eleArr.push(elements[i]);
        // 獲取子元素下的直接子元素
        var temp = childrenTag(elements[i]);  // 一層層的遞推下去
        eleArr = eleArr.concat(temp); // 將獲取的子元素的拼接到一起
    }
    return eleArr;
}
console.log(childrenTag(document.body)); // 打印的就是頁面body下所有的標籤
// 獲取所有標籤
var tags=childrenTag(document.body);
// 給所有標籤添加邊框
for(var i=0;i<tags.length;i++){
    tags[i].style.border='1px solid cyan';
}

效果圖:

image

12. JS 內存管理

本章引用自:《MDN-內存管理》

12.1 內存生命週期

不管是什麼程序語言,內存生命週期基本是一致的:
  • 分配你所需要的內存;
  • 使用分配到的內存(讀、寫);
  • 不需要時將其釋放、歸還。

JavaScript 的內存分配:

爲了不讓程序員費心分配內存,JavaScript在定義變量時就完成了內存分配。
var n = 123;        // 給數值變量分配內存
var s = "Levi";     // 給字符串分配內存

var o = {
  a: 1,
  b: null
};      // 給對象及其包含的值分配內存

// 給數組及其包含的值分配內存(就像對象一樣)
var a = [1, null, "abra"]; 

function f(a){
  return a + 2;
}   // 給函數(可調用的對象)分配內存

// 函數表達式也能分配一個對象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

使用值:

使用值的過程實際上是對分配內存進行讀取與寫入的操作。讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。

當內存不再需要使用時釋放:

大多數內存管理的問題都在這個階段。在這裏最艱難的任務是找到“所分配的內存確實已經不再需要了”。它往往要求開發人員來確定在程序中哪一塊內存不再需要並且釋放它。

高級語言解釋器嵌入了“垃圾回收器”,它的主要工作是跟蹤內存的分配和使用,以便當分配的內存不再使用時,自動釋放它。這隻能是一個近似的過程,因爲要知道是否仍然需要某塊內存是無法判定的(無法通過某種算法解決)。

12.2 垃圾回收

如上所述,自動尋找是否一些內存“不再需要”的問題是無法判定的。因此,垃圾回收實現只能有限制的解決一般問題。本節將解釋必要的概念,瞭解主要的垃圾回收算法和它們的侷限性。

1、引用:

垃圾回收算法主要依賴於引用的概念。在內存管理的環境中,一個對象如果有訪問另一個對象的權限(隱式或者顯式),叫做一個對象引用另一個對象。例如,一個Javascript對象具有對它原型的引用(隱式引用)和對它屬性的引用(顯式引用)。

在這裏,“對象”的概念不僅特指JavaScript對象,還包括函數作用域(或者全局詞法作用域)。

2、引用計數垃圾收集:

這是最天真的垃圾收集算法。此算法把“對象是否不再需要”簡化定義爲“對象有沒有其他對象引用到它”。如果沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。
  • 示例代碼
var o = { 
  a: {
    b:2
  }
}; 
// 兩個對象被創建,一個作爲另一個的屬性被引用,另一個被分配給變量o
// 很顯然,沒有一個可以被垃圾收集


var o2 = o; // o2變量是第二個對“這個對象”的引用

o = 1;      // 現在,“這個對象”的原始引用o被o2替換了

var oa = o2.a; // 引用“這個對象”的a屬性
// 現在,“這個對象”有兩個引用了,一個是o2,一個是oa

o2 = "yo"; // 最初的對象現在已經是零引用了
           // 他可以被垃圾回收了
           // 然而它的屬性a的對象還在被oa引用,所以還不能回收

oa = null; // a屬性的那個對象現在也是零引用了
           // 它可以被垃圾回收了
  • 限制:循環引用
該算法有個限制:無法處理循環引用。在下面的例子中,兩個對象被創建,並互相引用,形成了一個循環。它們被調用之後會離開函數作用域,所以它們已經沒有用了,可以被回收了。然而,引用計數算法考慮到它們互相都有至少一次引用,所以它們不會被回收。
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();
  • 實際例子:
IE 6, 7 使用引用計數方式對DOM對象進行垃圾回收。該方式常常造成對象被循環引用時內存發生泄漏
var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

在上面的例子裏,myDivElement這個DOM元素裏的circularReference屬性引用了myDivElement,造成了循環引用。如果該屬性沒有顯示移除或者設爲null,引用計數式垃圾收集器將總是且至少有一個引用,並將一直保持在內存裏的DOM元素,即使其從DOM樹中刪去了。如果這個DOM元素擁有大量的數據(如上的lotsOfData屬性),而這個數據佔用的內存將永遠不會被釋放。

3、標記-清除算法

這個算法把“對象是否不再需要”簡化定義爲“對象是否可以獲得”。

這個算法假定設置一個叫做根(root)的對象(在Javascript裏,根是全局對象)。垃圾回收器將定期從根開始,找所有從根開始引用的對象,然後找這些對象引用的對象……從根開始,垃圾回收器將找到所有可以獲得的對象和收集所有不能獲得的對象。

這個算法比前一個要好,因爲“有零引用的對象”總是不可獲得的,但是相反卻不一定,參考“循環引用”。

2012年起,所有現代瀏覽器都使用了標記-清除垃圾回收算法。所有對JavaScript垃圾回收算法的改進都是基於標記-清除算法的改進,並沒有改進標記-清除算法本身和它對“對象是否不再需要”的簡化定義。

  • 循環引用不再是問題了
在上面的示例中,函數調用返回之後,兩個對象從全局對象出發無法獲取。因此,他們將會被垃圾回收器回收。第二個示例同樣,一旦div和其事件處理無法從根獲取到,他們將會被垃圾回收器回收。
  • 限制: 那些無法從根對象查詢到的對象都將被清除
儘管這是一個限制,但實踐中我們很少會碰到類似的情況,所以開發者不太會去關心垃圾回收機制。

一般情況下, 如果需要手動釋放變量佔用的內存, 就將這個變量賦值爲:null

13. 閉包

瞭解閉包之前,先了解下另外兩個知識點:

1、函數基礎知識

  • 1、函數內部的代碼在調用的時候執行
  • 2、函數返回值類型可以是任意類型
  • 3、怎麼理解函數的返回值

    • 將函數內部聲明的變量暴露到函數外部
    • 函數內用來返回數據,相當於沒有函數的時候直接使用該數據
    • 不同之處在於:函數形成作用域,變量爲局部變量
function foo() {
    var o = {age: 12};
    return o;
}
var o1 = foo();

// 相當於: var o1 = {age: 18};

2、作用域的結論

  • 1、JavaScript的作用域是詞法作用域
  • 2、函數纔會形成作用域(函數作用域)
  • 3、詞法作用域:變量(變量和函數)的作用範圍在代碼寫出來的就已經決定, 與運行時無關
  • 4、函數內部可以訪問函數外部的變量(函數外部不能訪問函數內部的變量)
  • 5、變量搜索原則:從當前鏈開始查找直到0級鏈
  • 6、當定義了一個函數,當前的作用域鏈就保存起來,並且成爲函數的內部狀態的一部分。

13.1 閉包的概念

閉包從字面意思理解就是閉合,包起來。簡單的來說閉包就是,一個具有封閉的對外不公開的包裹結構或空間。

JavaScript中函數可以構成閉包。一般函數是一個代碼結構的封閉結構,即包裹的特性。同時根據作用域規則, 只允許函數訪問外部的數據,外部無法訪問函數內部的數據,即封閉的對外不公開的特性。因此說函數可以構成閉包。

閉包的其他解釋

在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。
  • 這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。
  • 閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例

實例:

function fn() {
    var num = 123;
    return function foo() {
        console.log(num);
    };
}

// bar1就是閉包的一個實例
var bar1 = fn();
// bar2就是閉包的另外一個實例
var bar2 = fn();

bar1(); // 123
bar2(); // 123

閉包的構成

閉包包括兩部分:
  • 1、函數體(函數自身的代碼);
  • 2、環境(函數的作用域)。

閉包的說明

  • 1、JS中函數形成了閉包
  • 2、閉包是函數作用域的應用
  • 3、對於閉包來說,只關注創建函數的作用域,不關注調用函數的位置

閉包的作用

  • 對函數內部的變量起到保護作用
  • 除了返回的函數以外,沒有任何手段能夠獲取或者修改這個變量的值

13.2 閉包模型

function foo() {
    var num = 0;
    // 函數會產生一個作用域,所以外部的程序想要訪問函數內部的變量,一般情況下是不行的
    // 通過閉包的方式可以使外部訪問到函數內部的變量
    // 具體做法就是在函數內部返回一個函數,並且這個函數使用了這個變量
    // 當用戶調用最外層的函數的時候,使用的這個變量就會隨着返回的函數返回給用戶
    return function() {
        return ++num;
    };
}
// 函數foo的返回值就是一個函數,所以,就可以調用getNum這個函數了!
var getNum = foo();
console.log(getNum());   // 1

13.3 閉包的使用

目標:想辦法(在外部)訪問到函數內部的數據

利用函數返回值

function foo() {
    var num = Math.random();
    return num;
}

var num1 = foo();
var num2 = foo();
console.log(num1 === num2);  // 隨機數 相同的情況很小很小

普通的函數返回值說明

  • 兩次調用函數,返回的數據並不是同一個數據。
  • 原因:函數在每次調用的時候,內部的數據會被新創建一次

遊戲充值案例

  • 示例圖片:

image

  • 示例代碼:
<button id="pay">充值</button>
<button id="play">玩遊戲</button>
<script>
    // 需求:
    // 1-需要對充值的金額起到保護作用,這個存放數值的變量不能暴露在全局,否則誰都會去修改這個金額
    //   var money = 0;
    // 2-點擊充值按鈕的時候,每次充值10元
    // 3-點擊玩遊戲按鈕的時候,每玩一次金額減少一元
    function fn (){
        var money = 0; // money用來存儲充值的錢,放在函數內部,不會暴露在全局
        // 一般的閉包返回值是一個函數,但是這裏有兩個功能,一個是玩遊戲,一個是充值;
        // 兩個功能分開,但是金額之間還是關聯的,所以這裏返回一個對象,裏面存放兩個方法
        return {
            // 充值的函數
            recharge:function(value){
                money += value;
                console.log('尊敬的黃金會員,您本次充值:' + value, ',您的總餘額爲:' + money);
            },
            // 玩遊戲的函數
            play:function(){
                if(money <= 0){
                    console.log('餘額不足無法繼續遊戲,請充值!');
                    return;
                }
                money--;
                console.log('您還剩餘 ' + money + ' 條命!');
            }
        };
    }
    var obj = fn();
    // 點擊“充值”按鈕
    var pay = document.getElementById('pay');
    pay.addEventListener('click', function () {
        obj.recharge(10);
    });
    // 點擊“玩遊戲”按鈕
    var play = document.getElementById('play')
    play.addEventListener('click', function () {
        obj.play();
    })
</script>
  • 優化,多個角色進行充值玩遊戲
<div>
    <button id="pay">小明:充值</button>
    <button id="play">小明:玩遊戲</button>
</div>

<div>
    <button id="pay1">小華:充值</button>
    <button id="play1">小華:玩遊戲</button>
</div>

<script>
    // 1 需要對充值的錢起到保護作用
    // var money = 0;
    // 2 充值:    每次充值20
    // 3 玩遊戲:  每玩一次,金額少1
    
    // 整個fn()形成一個函數作用域,對裏面的變量起到保護作用
    function fn() {
      // money 用來存儲充值的錢
      var money = 0;
    
      // 充值的函數:
      function recharge(value) {
        // money += 20;
        money += value;
        console.log('尊敬的黃金會員,您本次充值:' + value, ',您的總餘額爲:' + money);
      }
    
      // 玩遊戲的函數
      function play() {
        money--;
        if (money < 0) {
          console.log('餘額不足,請充值!');
        } else {
          console.log('您還剩餘 ' + money + ' 條命!');
        }
      }
    
      return {
        recharge: recharge,
        play: play
      };
    }
    
    // 小明充值玩遊戲的函數
    var obj;
    obj = fn();
    
    // 小明玩遊戲:
    var pay = document.getElementById('pay');
    pay.addEventListener('click', function () {
      obj.recharge(20);
    });
    
    var play = document.getElementById('play')
    play.addEventListener('click', function () {
      obj.play();
    });
    
    // 小華(新的閉包實例):
    var obj1 = fn();
    // 小華玩遊戲:
    var pay1 = document.getElementById('pay1');
    pay1.addEventListener('click', function () {
      obj1.recharge(20);
    });
    
    var play1 = document.getElementById('play1')
    play1.addEventListener('click', function () {
      obj1.play();
    });
</script>

優化的案例我們可以看到,只要重新定義一個變量,接收函數 fn(),就能重新開闢一個新的空間,且多個用戶之間不受任何影響。

13.4 閉包裏的緩存

從內存看閉包

函數調用也是需要內存的!因爲函數中聲明瞭一些變量,這些變量在函數調用過程中是可以使用的,所以, 這個變量是存儲到了函數調用時候分配的內存中了!因爲沒有任何變量來引用這塊內存,所以,函數調用結束。 函數調用佔用的內存就會被回收掉。

雖然,此時的函數有返回值(返回了一個普通的變量),並且這個函數調用結束以後這個函數佔用的內存還是被回收了!但是, 存儲函數的內存還在。

閉包的內存佔用:

作用域的引用是對函數整個作用域來說的,而不是針對作用域中的某個變量!!!即便沒有任何的變量,也是有作用域( 作用域的引用 )。
function fn() {
    var num = 123;
    return function() {
        console.log(num);
    };
}
// 此時, 函數fn調用時候佔用的內存, 是不會被釋放掉的!!!
var foo = fn();
// 調用 foo() 此時, 因爲返回函數的作用域對外層函數fn的作用域有引用
// 所以, 即使是 fn() 調用結束了, 因爲 返回函數作用域引用的關係, 所以
// 函數fn()調用時候, 產生的內存是不會被釋放掉的!
foo();

// 手動釋放閉包占用的內存!
foo = null;

緩存介紹

  • 緩存:暫存數據方便後續計算中使用。
  • 緩存中存儲的數據簡單來說就是:鍵值對
  • 工作中,緩存是經常被使用的手段。
  • 目的:提高程序運行的效率
  • 我們只要是使用緩存,就完全信賴緩存中的數據。所以, 我們可以通過閉包來保護緩存。

對於緩存來說,我們既要存儲值,又要取值!存儲的目的是爲了將來取出來,在js中可以使用對象或者數組來充當緩存。

如果是需要保持順序的,那麼就用數組,否則就用對象!

// 創建一個緩存:
var cache = {};

// 往緩存中存數據:
cache.name = 'xiaoming';
cache['name1'] = 'xiaohua';

// 取值
console.log(cache.name);
console.log(cache['name1']);

計算機中的緩存就是數據交換的緩衝區(稱作Cache),當某一硬件要讀取數據時,會首先從緩存中查找需要的數據,如果找到了則直接執行,找不到的話則從內存中找。由於緩存的運行速度比內存快得多,故緩存的作用就是幫助硬件更快地運行。

緩存使用步驟

  • 首先查看緩存中有沒有該數據,
  • 如果有,直接從緩存中取出來;
  • 如果沒有就遞歸計算,並將結果放到緩存中

遞歸計算斐波那契數列存在的問題

前面在學習遞歸的時候,我們舉了一個斐波那契數列的例子,但是當時說存在性能問題,我們重新看下這個問題。
// 使用遞歸計算 菲波那契數列
// 數列:1 1 2 3 5 8 13 21 34 55 89 。。。
// 索引:0 1 2 3 4 5 6  7  8  9  10 。。。

var count = 0;
var fib = function (num) {
  count++;
  if (num === 0 || num == 1) {
    return 1;
  }

  return fib(num - 1) + fib(num - 2);
};

// 計算索引號爲10的值, 一共計算了:  177 次
// 計算索引號爲11的值, 一共計算了:  287 次
// 計算索引號爲12的值, 一共計算了:  465 次
// ....
// 計算索引號爲20的值, 一共計算了:  21891 次
// 計算索引號爲21的值, 一共計算了:  35421 次
// ...
// 計算索引號爲30的值, 一共計算了:  2692537 次
// 計算索引號爲31的值, 一共計算了:  4356617 次

fib(31);
console.log(count); // 4356617

注意上面代碼,count是用來記錄程序運行時執行的次數,不明白的小夥伴可以返回遞歸那一章節,我專門畫了一張圖,可以理解下這個次數是怎麼計算的。我們看下上面的代碼的註釋,求第20項跟21項的時候,雖然只相差一項,但是卻多運算了一萬多次,試想一下這裏面存在的效率問題是多麼的可怕。

閉包和緩存解決計算斐波那契數列存在的問題

其實主要的問題就是,數據重複運算。比如計算第五項的時候,他計算的是第三項跟第四項的和,這時的第三項跟第四項都是從一開始重新計算的,假如吧計算過得值保存下來,就不需要再重複的運算。
  • 運用緩存:將計算的值存儲下來,減少運算次數,提高效率;
  • 使用閉包:從來保護緩存。
// 記錄計算的次數
var count = 0;

function fn() {
  // 緩存對象
  var cache = {};

  // 這個返回函數纔是 遞歸函數!
  return function( num ) {
    count++;

    // 1 首先查看緩存中有沒有 num 對應的數據
    if(cache[num]) {
      // 說明緩存中有我們需要的數據
      return cache[num];
    }

    // 2 如果緩存中沒有, 就先計算, 並且將計算的結果存儲到緩存中
    if(num === 0 || num === 1) {
      // 存儲到緩存中
      cache[num] = 1;
      return 1;
    }

    var temp = arguments.callee(num - 1) + arguments.callee(num - 2);
    cache[num] = temp;
    return temp;
  };
}

var fib = fn();
var ret = fib(20);
console.log(ret);   // 10946
console.log('計算了:' , count, '次');  // 計算了: 39 次

我們可以跟上面沒有使用緩存,求斐波那契數列的比較一下,此時求第20項的時候,僅僅運算了39次,但是在之前卻運行了21891次。

上面的方法存在着一些的問題,每次在執行的時候,函數fn都要先被調用一次(var fib = fn();),下面進行優化:

  • fn轉換成自執行函數(沙箱模式,下一章會講),自執行函數的返回函數就是遞歸函數;
  • 判斷緩存是否存在的條件進行優化,之前是通過判斷緩存的值是否存在,來進行存、取值的,但是假如一個緩存的值是false的時候呢?豈不是if(false){}了,明明有值的時候,卻不能取值了,所以玩我們只需要判斷緩存裏是否存在某個鍵就行。
var fib = (function () {
  // 緩存對象
  var cache = {};

  // 這個返回函數纔是 遞歸函數!
  return function (num) {
    // 1 首先查看緩存中有沒有 num 對應的數據

    /**
        if(cache[num]) {
          return cache[num];
        }
    */
    // 只要緩存對象中存在 num 這個key, 那麼結果就應該是 true
    if (num in cache) {
      // 說明緩存中有我們需要的數據
      return cache[num];
    }

    // 2 如果緩存中沒有, 就先計算, 並且將計算的結果存儲到緩存中
    if (num === 0 || num === 1) {
      // 存儲到緩存中
      // cache[num] = 1 是一個賦值表達式, 賦值表達式的結果爲: 等號右邊的值!
      return (cache[num] = 1);
    }

    // arguments.callee 表示當前函數的引用
    return (cache[num] = arguments.callee(num - 1) + arguments.callee(num - 2));
  };
})();

var ret = fib(10)
console.log(ret);

什麼是 arguments.callee?

返回正被執行的function對象,也就是所指定的function對象的正文。callee屬性是arguments 對象的一個成員,它表示對函數對象本身的引用,這有利於匿名函數的遞歸或者保證函數的封裝性。
function fn(a, b) {
    console.log(arguments);
}
fn(1, 2);

我們可以看到,打印的arguments屬性裏面有哪些參數:

arguments

  • 前面幾項是函數調用後傳進來的實參;
  • callee:f,它其實就是函數fn的引用,你可以理解爲:arguments.callee()相當於fn()
  • length就是實參的長度。

再去看上面斐波那契的案例,它的遞歸函數是一個匿名函數,所以在這個函數裏面自己調用自己的時候,就是使用的arguments.callee去引用的。

14. 沙箱模式

沙箱模式又稱:沙盒模式、隔離模式。沙箱(sandbox)介紹:用於爲一些來源不可信、具備破壞力或無法判定程序意圖的程序提供試驗環境。然而,沙盒中的所有改動對操作系統不會造成任何損失。

14.1 沙箱模式的作用

  • 作用:對變量進行隔離
  • 問題:在js中如何實現隔離?
ES6之前, JavaScritp中只有函數能限定作用域,所以,只有使用函數才能實現隔離。

本質上還是對函數作用域的應用。

14.2 沙箱模式模型

  • 使用自調用函數實現沙箱模式

    • 函數形成獨立的作用域;
    • 函數只有被調用,內部代碼纔會執行;
    • 將全局污染降到最低。
(function() {
    // ...
    // 代碼
    // ...
})(); 

14.3 沙箱模式應用

最佳實踐:在函數內定義變量的時候,將 變量定義 提到最前面。
// 1 減少了window變量作用域的查找
// 2 有利於代碼壓縮
(function( window ) {

  var fn = function( selector ) {
    this.selector = selector;
  };

  fn.prototype = {
    constructor: fn,
    addClass: function() {},
    removeClass: function() {}
  };

  // 給window添加了一個 $屬性,值爲: fn
  // 暴露數據的方式:
  window.$ = fn;

})( window );

14.4 沙箱模式的說明

  • 將代碼放到一個立即執行的函數表達式(IIFE)中,這樣就能實現代碼的隔離;
  • 使用IIFE:減少一個函數名稱的污染,將全局變量污染降到最低;
  • 代碼在函數內部執行,函數內部聲明的變量不會影響到函數外部;
  • 如果外部需要,則可以返回數據或把要返回的數據交給window

IIFE: Immediately Invoke Function Expression立即執行的函數表達式

15. 工廠模式

工廠模式是一種設計模式,作用是:隱藏創建對象的細節,省略了使用new創建對象。

構造函數:

構造函數創建之後,我們實例化一個對象的時候都是直接通過new創建出來的。
function Person(name, age) {
  this.name = name;
  this.age = age;
}
var p1 = new Person('Levi', 18);  

工廠函數:

工廠函數的核心就是隱藏這個new創建對象的細節。
function Person(name, age) {
  this.name = name;
  this.age = age;
}

function createPerson(name, age) {
    return new Person(name, age);
}

var p2 = createPerson('Ryan', 19);

兩段代碼比較下來,我們可以看到,實例出來的p2對象沒有直接使用new創建,而是通過一個函數的返回值創建出來的,這就是工廠模式。

使用場合:

jQuery中,我們用的“$”或者jQuery函數,就是一個工廠函數。
/* Jquery 中的部分源碼 */
// jQuery 實際上是一個 工廠函數,省略了 new 創建對象的操作
jQuery = function( selector, context ) {
    // jQuery.fn.init 纔是jQuery中真正的構造函數
    return new jQuery.fn.init( selector, context );
}

(本篇完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章