前端點滴(JS進階)(三)---- JavaScript 兩鏈一包

一、作用域鏈

1. 作用域的概念

作用域就是代碼的執行環境,全局執行環境就是全局作用域,函數的執行環境就是局部作用域,它們都是棧內存。

概括來說:

  • 局部作用域 ===> 函數執行都會形成一個局部作用域
  • 全局作用域 ===> 頁面一打開就會形成一個全局作用域
  • 局部變量 ===> 在局部作用域裏邊形成的變量 (通過 var 聲明; 函數的形參)
  • 全局變量 ===> 在全局作用域形成的變量(var a = 12 或者函數內沒有聲明,直接賦值的變量)

作用域規則:

  • 規則一:函數可以使用函數以外的變量。(作用域鏈的查找變量)
  • 規則二:函數內部,優先使用函數內部的變量。
  • 規則三:函數內部也會發生變量提升。
  • 規則四:函數內部沒有用var聲明的變量,也是全局變量。
  • 規則五:外部環境不能訪問內部環境的任何變量和函數。

實例:

/* 規則一:函數可以使用函數以外的變量。(作用域鏈的查找變量)。 */
var a = 10;
function fn(){
	console.log(a);
}
fn();

//=>  10
/* 規則二:函數內部,優先使用函數內部的變量。 */
var a = 10;
function fn(){
	var a = 20;
	console.log(a);
}
fn();

//=>  20
/* 規則三:函數內部也會發生變量提升。 */
var a = 10;
function fn(){
	console.log(a);
	var a = 20;
}
fn();

//=>  undefined
/* 規則四:函數內部沒有用var聲明的變量,也是全局變量。 */
var a = 10;
function fn(){
	console.log(a);
}
function fn2(){
	a = 20;
}
fn2();
fn();

//=>  20
/* 規則五:外部環境不能訪問內部環境的任何變量和函數。 */
function fn(){
	var a = 10;
}
fn();
console.log(a);  //=>   "error"  "ReferenceError: a is not defined

2. 作用域鏈

作用域鏈實際上就是一種查找方式
在內部函數中查找變量的時候,優先從函數內部自身查找,如果沒有查到,則向外層查找,如果外層還沒有,則繼續向上一層查找,一直查詢到全局作用域。這種鏈式的查找方式就是作用域鏈。值得注意的是: 函數內部也會發生變量提升。並且嚴格遵照js自上而下的執行順序。

先來看一個簡單的實例:

/* 實例一 */
var a = 10;
function fn1(){
    var a = 20;
    function fn2(){
        console.log(a);
    }
    fn2();
    a = 30;
    console.log(a);
}
fn1();

畫個圖來表示表示:
在這裏插入圖片描述
所以:輸出 20,30

/* 實例二 */
var a = 10;
function fn1(){
    function fn2(){
        console.log(a);
    }
    fn2();
    a = 30;
    console.log(a);
}
fn1();

同樣道理:優先從函數內部自身查找,如果沒有查到,則向外層查找,如果外層還沒有,則繼續向上一層查找,一直查詢到全局作用域。
輸出:10 30

/* 實例三 */
var a = 10;
function fn1(){
    function fn2(){
        console.log(a);
    }
    fn2();
    var a = 30;
    console.log(a);
}
fn1();

函數內部會發生變量提升。輸出: undefined 30

二、面向對象編程

面向對象編程就是基於對象的編程。面向對象編程簡稱OOP(Object-Oritened Programming)爲軟件開發人員敞開了一扇大門,它使得代碼的編寫更加簡潔、高效、可讀性和維護性增強。它實現了軟件工程的三大目標:(代碼)重用性、(功能)擴展性和(操作)靈活性,它的實現是依賴於面向對象的三大特性:封裝、繼承、多態。在實際開發中 使用面向對象編程 可以實現系統化、模塊化和結構化的設計 它是每位軟件開發員不可或缺的一項技能。

1. 知識回顧

JavaScript的基本類型(原始類型、值類型):
(string、number、boolean、undefined、null、symbol)
JavaScript的引用類型(對象類型、引用數據類型):
(String、Number、Boolean、Array、Function、Object、Date、RegExp、Match、Error)

JavaScript中的對象分爲:

  1. 普通對象:直接量語法得到的對象。比如:
var obj = {
	name:'張三',
	age:20,
	say:function(){
		console.log("會說法語")
	},
	sanwei:['100cm', '90cm', '105cm'],
	obj2:{name:"李四"}
};

ECMAScript-262 把對象定義爲:無序屬性的集合,其屬性可以包含各個類型的值、對象或者函數。

  1. 內置對象(函數對象):(String、Number、Boolean、Array、Function、Object、Date、RegExp、Match、Error)

普通對象與函數對象的區分:

  • 只要是Function的實例,那就是函數對象,其餘則爲普通對象。

實例:

const obj1 = {};
const obj2 = new Object();
function func1() {

}
const obj3 = new func1();
const func2 = new function() {

}
const func3 = new Function()

分別打印:

console.log(obj1);  // object
console.log(obj2);  // object
console.log(obj3);  // object
console.log(func1);  // function
console.log(func2);  // object
console.log(func3);  // function

2. 定義對象

(1)new 內置對象

之前學習過的String對象、Date對象、Array對象、RegExp對象。使用這些對象的時候,可以new這些函數。然後將得到的返回值當做對象來使用。比如使用字符串對象:

var str = new String('hello world'); // 通過new內置的函數,得到對象。

(2)直接量語法

直接量語法定義的對象,值可以是任何的數據類型:

var obj = {
	name:'張三',
	age:20,
	say:function(){
		console.log("會說法語");
	},
	sanwei:['100cm', '90cm', '105cm'],
	obj2:{name:"李四"}
};

注意:直接量語法中的this指向當前對象。

var obj = {
	name:'張三',
	age:20,
	say:function(){
		console.log("會說法語,"+this.age+"歲小夥");
	},
	sanwei:['100cm', '90cm', '105cm'],
	obj2:{name:"李四"}
};
obj.say();   //=>  "會說法語,20歲小夥"

(3)Es5 new 構造函數

ES5中沒有類的概念,只有構造函數或構造器。要想得到對象,只能new一個構造函數。
什麼是構造函數?什麼是普通函數?
定義函數的時候,正常按照函數的語法來定義即可。如果這個函數正常使用,那麼還是一個函數,如果一個函數被new了,那麼這個函數就可以叫做構造函數。

function func (){  //func稱爲構造函數

};
var fn = new func();
console.log( typeof func.prototype); // object
console.log(typeof fn.__proto__);  // object

(4)Es6 Class(類)

ES6 引入了class(類),讓JavaScript的面向對象編程變得更加簡單和易於理解。

class Animal{
    constructor(){
    	this.name ="dog";
    	this.color ="white";
    };
    toString(){
        console.log('name:'+this.name +',color:'+this.color);
    };
}

var animal =new Animal();
animal.toString();  // "name:dog,color:white"
console.log(typeof animal.__proto__);  //object
console.log(typeof Animal.prototype);  //object

3. 對象的相關操作

參考博客:https://blog.csdn.net/Errrl/article/details/103827729

4. 對象在內存中的存在形式

對象在傳值上,是 引用傳遞。在使用對象的時候,實際上都是使用的對象的地址。
代碼:

/* 實例一 */
/* 根據構造函數得到兩個對象 */
    //定義構造函數
    function Person(n, a) {
        this.name = n;
        this.age = a;
        this.say = function () {
            console.log(123456);
        }
    }
    //實例化,得到對象
    var p1 = new Person('張三', 20);
    var p2 = new Person('李四', 25);

得到的兩個對象在內存中的形式:
在這裏插入圖片描述
在實際使用對象的時候,實際上都是使用的對象的地址。

/* 實例二 */
//定義構造函數
    function Person(n, a) {
        this.name = n;
        this.age = a;
        this.say = function () {
            console.log(123456);
        }
    }
    //實例化,得到對象
    var p1 = new Person('張三', 20);
    var p2 = new Person('李四', 25);

    var p3 = p1;
    p3.name = '王五';
    console.log(p1.name, p3.name);  // "王五"  "王五"

在這裏插入圖片描述

/* 實例三 */
//定義構造函數
    function Person(n, a) {
        this.name = n;
        this.age = a;
        this.say = function () {
            console.log(123456);
        }
    }
    //實例化,得到對象
    var p1 = new Person('張三', 20);
    var p2 = new Person('李四', 25);

    var p3 = p1;
    p3 = null;  //實際上是將p3指向堆區的引用切斷,而不會影響到p1
    console.log(p1);   //=>  {name:"張三",age:20,say:f}

在這裏插入圖片描述
把對象當做參數傳給函數,實際上傳遞的也是地址。

/* 實例四 */
//定義構造函數
    function Person(n, a) {
        this.name = n;
        this.age = a;
        this.say = function () {
            console.log(123456);
        }
    }
    //實例化,得到對象
    var p1 = new Person('張三', 20);
    var p2 = new Person('李四', 25);

    function change(o){
        o.name = '王五';
    }
    change(p1);
    console.log(p1.name);  //=> "王五"

5. 構造函數

實際上就是一個對象的架構框架。

function fn(name,age){
	this.name = name;  // 私有屬性
	this.age = age;  // 私有屬性
	this.say = function(){   //公有屬性
		console.log("會講粵語")
	}
}
fn.prototype.tellMeAge= function(){
	console.log(this.age);
}
var fnc = new fn("yaodao",20);
console.log(fnc.name);  //=> "yaodao"
fnc.tellMeAge();  //=>  20
console.log(fnc);  //=>  {name: "yaodao", age: 20, say: ƒ}

6. 原型對象

(1)沒有利用原型對象的情況

在實例化得到一個對象的時候,會爲這個對象分配一個原型對象。
不用就等於浪費。

/* 實例 :一個構造函數,實例化得到三個對象。*/
function Person(n, a) {
        this.name = n;
        this.age = a;
        this.say = function () {
            console.log(123456);
        };
        this.cook = function () {
            console.log('我做得一手好飯');
        };
        //....
    }
var p1 = new Person('張三', 20);
var p2 = new Person('李四', 25);
var p3 = new Person('王五', 28);

在內存中的形式:
在這裏插入圖片描述
在內存中,會分別爲每個對象開闢新的空間。發現每個對象中的say和cook都一樣,這樣的話,會佔用大量的內存。解決辦法就是使用原型對象。

(2)獲取原型對象

獲取原型的方法有兩種:__proto__以及 prototype
區別就是:
在這裏插入圖片描述

  • 只有函數對象有 prototype 屬性,普通對象 沒有這個屬性。
  • 函數對象 和 普通對象 都有 __proto__這個屬性。
  • prototype 和 __proto__都是在創建一個函數(構造函數)或者對象會自動生成的屬性。

(3)利用原型對象

在實例化得到對象的時候,系統會爲構造器創建一個對象,該對象會保存構造器的每個實例對象的相同內容,這個對象就是原型對象。所以有了原型對象,再定義構造函數的時候,就可以將每個對象獨有的內容放到構造函數中,將每個對象相同的內容都放到原型對象上,以達到節省內存佔用的效果。
按照上述方法利用原型對象節省內存的佔用。

//構造函數
    function Person(n, a) {
        this.name = n;
        this.age = a;
    }
    //把say和cook放到Person的原型對象上
    Person.prototype.say = function(){
        console.log(123456);
    };
    Person.prototype.cook = function(){
        console.log('我做得一手好飯');
    };
    //實例化三個對象
    var p1 = new Person('張三', 20);
    var p2 = new Person('李四', 25);
    var p3 = new Person('王五', 28);

    //測試say和cook是否能正常使用
    p1.say();   //=> 123456
    p3.cook();  //=> 我做得一手好飯

在內存中的形式:
在這裏插入圖片描述
由上可見,原型對象不能單獨存在,肯定要和構造函數產生關係才行,而構造函數又與實例化對象有關係。
所以它們的關係如下:
在這裏插入圖片描述

function fn(){/* 構造函數 */}
var fnc = new fn();
console.log(fn.prototype===fnc.__proto__);  //=>  true
console.log(fn.prototype.constructor===fnc.constructor);  //=>  true

7. 原型鏈

所謂的原型鏈實際上也是一種查找方式。完整的原型鏈如下:
在這裏插入圖片描述
那到底怎麼查找呢?
找一個對象的屬性時:

  1. 優先從對象自身查找;
  2. 然後從對象的構造函數中查找;
  3. 然後從構造函數的原型對象上查找;
  4. 然後從原型對象的構造函數中查找;
  5. ….
  6. 然後就是萬物祖宗 Object;
  7. Object 的原型對象;
  8. Object 原型對象的原型對象 null;(萬物皆空)

先來看看一個實例:

/* 實例一 */
function A() {
    this.age = 10;
}
function B(){
    this.age = 20;
}
    
//指定B的原型對象爲A的實例
B.prototype = new A();
//實例化B,得到對象
var b = new B();
console.log(b.age);  
console.log(b);

在這裏插入圖片描述
輸出:
在這裏插入圖片描述

/* 實例二 */
function A() {
    this.age = 10;
}
function B(){

}
    
//指定B的原型對象爲A的實例
B.prototype = new A();
//實例化B,得到對象
var b = new B();
console.log(b.age);  
console.log(b);

輸出:
在這裏插入圖片描述

/* 實例三 */
function A() {
    this.age = 10;
}
function B(){

}
B.prototype.age = 20;
//指定B的原型對象爲A的實例
B.prototype = new A();
//實例化B,得到對象
var b = new B();
console.log(b.age);  
console.log(b);

輸出:
在這裏插入圖片描述

/* 實例四 */
function A() {}
function B(){}
B.prototype.age = 20;
//指定B的原型對象爲A的實例
B.prototype = new A();
//實例化B,得到對象
var b = new B();

輸出:
在這裏插入圖片描述

/* 實例五 */
function A() {

}
function B(){
B.prototype.age = 20;
}

//指定B的原型對象爲A的實例
B.prototype = new A();
//實例化B,得到對象
var b = new B();
console.log(b.age);  
console.log(b);

輸出:
在這裏插入圖片描述

/* 實例六 */
function A() {
    this.age = 5
}
A.prototype.age = 10;
A.prototype.age = 15;

function B(){
    this.age = 20;
    B.prototype.age = 25;
}
B.prototype.age = 30;

//指定B的原型對象爲A的實例
B.prototype = new A();
B.prototype.age = 35;
//實例化B,得到對象
var b = new B();
console.log(b.age);  
console.log(b);

注意:
A.prototype === a.__proto__
B.prototype === b.__proto__
內存圖形式:
在這裏插入圖片描述

輸出:
在這裏插入圖片描述
資料參考:https://segmentfault.com/a/1190000015642813

三、閉包

1. JavaScript 垃圾回收機制

(1)JS 的垃圾回收機制的基本原理

找出那些不再繼續使用的變量,然後釋放其佔用的內存,垃圾收集器會按照固定的時間間隔週期性地執行這一操作。

(2)回收方式

標記清除
當變量進入環境時,將這個變量標記爲“進入環境”;當變量離開環境時,則將其標記爲“離開環境”。標記“離開環境”的就回收內存。

引入計數(低級瀏覽器)
另外一種不太常見的垃圾收集策略叫引用計數(Reference Counting),此算法把“對象是否不再需要”簡化定義爲“對象有沒有其他對象引用到它”。如果沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。
引用計數的策略是跟蹤記錄每個值被使用的次數,當聲明瞭一個變量並將一個引用類型賦值給該變量的時候這個值的引用次數就加 1,如果該變量的值變成了另外一個,則這個值得引用次數減 1,當這個值的引用次數變爲 0 的時候,說明沒有變量在使用,這個值沒法被訪問了,因此可以將其佔用的空間回收,這樣垃圾回收器會在運行的時候清理掉引用次數爲 0 的值佔用的內存。
而引用計數的不繼續被使用,是因爲循環引用的問題會引發內存泄漏。
例如:

function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someObject = objB;
    objB.anotherObject = objA;
}

objA 和 objB 通過各自的屬性相互循環引用,也就是說,兩個對象的引用次數都是 2。在函數執行完畢後,objA, objB 還將繼續存在,因爲他們的引用計數永遠不會是 0。假如這個函數被多次執行,就會導致大量的內存得不到釋放。(內存泄漏)
不僅如此,造成內存泄漏的原因還有很多。

(3)內存泄漏的情況以及解決辦法

  1. 意外的全局變量引起的內存泄露
  • 原因:全局變量不會被回收。
  • 解決:使用嚴格模式避免。
  1. 閉包
  • 原因:一、閉包可以維持函數內部變量駐留內存,使其得不到釋放。二、活動對象被引用,使閉包內的變量不會被釋放
  • 解決:減少閉包的使用;將活動對象賦值爲null
function  showId() {
    var app = document.getElementById("app")
    app.onclick = function(){
      aler(app.id)   // 這樣會導致閉包引用外層的app,當執行完showId後,app無法釋放
    }
}

// 改成下面
function  showId() {
    var app = document.getElementById("app")
    var id  = app.id
    app.onclick = function(){
      aler(id) 
    }
    app = null    // 主動釋放app
}
  1. 被清理的DOM元素的引用
  • 原因: 雖然DOM被刪掉了,但對象中還存在對DOM的引用
  • 解決: 將對象賦值爲null
function click(){
// 但是 button 變量的引用仍然在內存當中。
const button = document.getElementById('button');
button.click();
button = null;  // 主動釋放button
}
// 移除 button 元素
function removeBtn(){
document.body.removeChild(document.getElementById('button'));
}
removeBtn();
click();
  1. 定時器未清除
  • 原因:定時器內部實現閉包,一直執行回調函數
  • 解決:清除定時器,定時器對象賦值爲null

2. 什麼是閉包?

我們都知道,js的作用域分兩種,全局和局部,基於我們所熟悉的作用域鏈相關知識,我們知道在js作用域環境中訪問變量的權利是由內向外的,內部作用域可以獲得當前作用域下的變量並且可以獲得當前包含當前作用域的外層作用域下的變量,反之則不能,也就是說在外層作用域下無法獲取內層作用域下的變量,同樣在不同的函數作用域中也是不能相互訪問彼此變量的,那麼我們想在一個函數內部也有限權訪問另一個函數內部的變量該怎麼辦呢?閉包就是用來解決這一需求的辦法之一,閉包的本質就是在一個函數內部創建另一個函數。

首先要清楚閉包的四大特性:

  1. 函數嵌套函數,內部函數通常被稱爲閉包函數,外部函數帶有內部函數的返回值。
  2. 函數內部可以引用函數外部的參數和變量
  3. 可以實現在全局變量下獲取到局部變量中的變量的值
  4. 參數和變量不會被垃圾回收機制回收

3. 使用閉包

那麼使用閉包有什麼好處呢?
使用閉包的好處是:

  1. 希望一個變量長期駐紮在內存中
  2. 避免全局變量的污染
  3. 私有成員的存在

說先來看看沒有使用閉包的情況:

function a(){
        var i = 1;
        console.log(i++);
    }
    a(); // 1    執行i++後,變量i被回收
    a(); // 1
    a(); // 1
    a(); // 1

原因就是在調用函數a時,函數執行完畢,變量被釋放造成var i = null(被回收機制回收),每次調用實際上只是console.log(null+1);

再來看看使用了閉包函數的情況:

function a(){
        var i = 1;
        function b(){
            console.log(i++);
        }
        return b;
    }

    var bb = a(); 
    bb();//1    執行i++後,變量i還在
    bb();//2
    bb();//3
    bb();//4
    bb = null;//釋放變量i

一般情況下,在函數a執行完後,就應該連同它裏面的變量一同被銷燬,但是在這個例子中,匿名函數作爲a的返回值被賦值給了bb,這時候相當於bb =function(){console.log(i++)},並且匿名函數內部通過原型鏈引用着a裏的變量 i ,所以變量 i 無法被銷燬,當程序執行完bb(), 這時候,a 和b 的執行環境纔會被銷燬。

接下來說說閉包的每個特性:
特性一: 函數嵌套函數,內部函數通常被稱爲閉包函數,外部函數帶有內部函數的返回值。
特性二:函數內部可以引用函數外部的參數和變量。
特性四:參數和變量不會被垃圾回收機制回收。

function foo(x) {
    var tmp = 3;
    function bar(y) {
        alert(x + y + (++tmp));
    }
    bar(10);
}
foo(2); //16
foo(2); //16
foo(2); //16

不管執行多少次,都會alert 16,因爲bar能訪問foo的參數x,也能訪問foo的變量tmp。
但,這還不是閉包。當你return的是內部function時,就是一個閉包。內部function會close-over(封閉)外部function的變量直到內部function結束。

function foo(x) {
    var tmp = 3;
    return function (y) {
        alert(x + y + (++tmp));
    }
}
var bar = foo(2); // bar 現在是一個閉包
bar(10);//16
bar(10);//17
bar(10);//18
bar = null;  // 釋放變量 tmp

特性三:可以實現在全局變量下獲取到局部變量中的變量的值。

var b = 2;
function fn(){
	var a = 1;
	return function (){
		return a;
	}
}
var fn2 = fn();
var b = fn2();
console.log(b);   //=> 1

4. 閉包實例

/* 實例一:通過作用域鏈實現對全局變量的自增 */
/* 原因:全局變量不會被回收 */
var a = 1;
function fn(){
	a++;
	console.log(a);
}
fn();//2
fn();//3
fn();//4
a = null;//釋放全局變量
/* 實例二:通過閉包實現對局部變量的自增 */
/* 原因:閉包造成局部變量不被釋放 */
var a = 1;
function fn(){
	a++;
	console.log(a);
}
fn();//2
fn();//3
fn();//4
a = null;//釋放全局變量
/* 案例三:通過閉包保存定時器回調函數中的變量 */
for(var i = 0;i<10;i++){
	setTimeout(function(){
		console.log(i);
	},1000)
}
//=> 輸出10個10,原因如果清楚Event Loop就清楚setTimeout在執行完
//=> 同步任務後執行,又由於for循環中的變量爲全局變量經過循環後i  =10
//=> setTimeout*10,所以輸出10個10 。

/* 使用了閉包的定時器可以在閉包函數內部保存變量 */
for(var i = 0;i<10;i++){
(function(i){
    setTimeout(function(){
		console.log(i);
	},1000)
})(i)
}
//=> 輸出0~9,就是因爲閉包函數使得變量不被釋放。

//=> 值得注意的是不僅setTimeout,還有事件監聽...也同樣需要閉包來實現需求。

5. 使用閉包的注意點

由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。

6. 閉包總結

在這裏插入圖片描述
在這裏插入圖片描述

發佈了37 篇原創文章 · 獲贊 6 · 訪問量 2218
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章