面向對象三、作用域


title: 面向對象三、作用域
date: 2017-06-17 10:10:13
tags: javascript筆記


instanceof 運算符

語法

object instanceof fn

如果運算符後面的函數的prototype屬性引用的對象出現在運算符面前對象的原型鏈上的話就返回true,否則返回false。

function foo (){

}
var f = new foo;
console.log(f instanceof foo);    // 返回true  判斷f是不是foo函數的實例
console.log(f instanceof Object);    // 返回true  f也是在Object的原型鏈上

function fn (){

}
foo.prototype = new fn;
var ff = new foo;
console.log(ff instanceof fn)     // true  因爲fn創建的對象就是foo.prototype,所以foo.prototype的原型就是fn.prototype。
console.log(ff instanceof Object)     // true
//ff -> foo.prototype -> fn.prototype -> Object.prototype -> null

作用域鏈

繪製作用域鏈的規則

  1. 將這個script標籤的全局作用域定義爲0級作用域鏈,將全局作用域上的所有數據(變量、對象、函數),繪製在這條鏈上

  2. 由於在詞法作用域中,只有函數可以分割作用域,那麼只要遇到函數就再引申出新的作用域鏈,級別爲當前鏈級別+1,將數據繪製到新鏈上

  3. 重複步驟二,直到沒有遇到函數爲止

以下面的函數舉例來繪製作用域鏈

var n = 123;
function f(){
  var n = 12;
  function f1(){
    var n = 1;
    function f2(){
      var n = 0;
    }
    function f3(){
      var n = 0;
    }
  }
}
1240
image

變量的搜索原則

  1. 當訪問一個變量時,首先在當前變量所處的作用域上查找,如果找到就直接使用,並停止查找

  2. 如果沒有找到就向上一級鏈(T-1)上去查找,如果找到就直接使用並停止查找

  3. 如果沒有找到就繼續向上一級鏈查找,直到0級鏈

  4. 如果沒有找到就報錯

  5. 如果訪問的變量不存在,會搜索整個作用域鏈(不僅性能低,而且拋出異常)

    • 在實際開發不推崇所有數據都寫在全局上。儘量使用局部變量,推薦使用沙箱。

    • 如果在開發中,所有js變量都寫在全局上,會造成全局污染

  6. 同級別的鏈上的變量互不干擾

function f (a){
  var a ;
  function a (){
    console.log(a);
  }
  a();
  a = 10;
  console.log(a);
}
f(100);
// 在這個題中 var a 不會覆蓋a的參數100,但是function會改變,a=10這個賦值操作也會覆蓋,因爲都相當於賦值。

補充

在函數執行時候,會創建一個執行的環境,這個環境包括:activeObject(活動對象)以及作用域鏈

activeObject存儲的是所有在函數內部定義的變量,以及函數的形參;

會將變量名字以及形參名字作爲該對象的屬性來存儲,比如有個變量a,那麼就等於有了a這個屬性,這時a的屬性值就是100;

因爲之前已經傳了a這個參數,傳了參數也相當於在函數內聲明瞭a這個變量,也就是說此時在activeObject中已經有了a這個屬性,所以這時在函數內聲明a就不管用了,只有賦值才管用。只能改屬性值但屬性不會再創建。上述代碼先將函數賦值給了a,又將100賦值給了a

查找對象也是在activeObject中查找,也就是查找裏邊的屬性和屬性值,沒有的話就找上一級函數的activeObject。直到找到爲止,沒有找到就報錯。

閉包

定義

  • 指一個函數有權去訪問另一個函數內部的參數和變量。

  • 創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量。

  • 應用閉包主要是爲了設計私有的方法和變量。

  • 一般函數執行完畢後,局部活動對象就被銷燬,內存中僅僅保存全局作用域,但是閉包的情況不同,不會被垃圾回收機制回收。

  • 爲了防止閉包導致的內存泄漏,用完閉包之後手工賦值爲null,就會被回收。

  • 閉包結構和閉包引用寫在同一個函數裏,出了函數就自動刪除該緩存了。

缺點

  • 閉包會造成函數內部的數據常駐內存,會增大內存使用量,從而引發內存泄漏問題。每創建一個閉包都會創建一個緩存數據,這樣就會造成內存泄漏(內存滿了後其他數據寫不進去)

  • 閉包會使變量始終保存在內存中,如果不當使用會增大內存消耗。

function fn (){
  var n = Math.random();
  function getN (){
    return n;  // 這個作用域中沒有n所以會向上尋找。
  }
  return getN;  //這裏是要返回整個getN函數,所以不加括號。
}

var ff = fn();  // 這個ff就是閉包,通過它可以訪問fn內部的數據。
var nn = ff();
var mm = ff();  // fn()實際上是getN這個函數體,那麼ff()就是調用了getN這個函數,這樣會返回n。
console.log(nn);
console.log(mm);  // nn和mm的數是相同的
console.log(nn === mm); //true,

ff = null; // n被回收

優點

  • 希望一個變量長期駐紮在內存中

  • 避免全局變量的污染

  • 私有成員的存在

閉包的應用

下面通過幾個案例來了解閉包的優點:

統計某個構造函數創建多少個對象,變量可以長駐內存

//統計某個構造函數創建多少個對象
function counter() {
  var n = 0;
  return {
    add:function(){
        n+=1;
    },
    getCounts:function(){
        return n;
    }
  }
}

// 創建一個閉包,相當於初始化計時器,因爲重新調用會讓n=0.
// 然後創建閉包時,n=0和return的對象會被緩存。
// 那麼爲什麼閉包環境能緩存數據呢:
// 因爲 var n = 0相當於n進入環境,在局部作用域創建了一個對象和n 最後把對象和n返回給外部作用域,相當於已出執行環境,通過全局變量就能找到返回的對象,通過返回的對象就能找到n,通過這個路徑就能找到變量n,
// 所以得出結論因爲在函數內部有方法(函數)對其有引用,並且又將其返回到外部作用域上的一個變量接收。創建之後就緩存了,這時再通過這個變量訪問閉包裏的環境,那麼只會訪問該變量的緩存區域。
var PresonCount = counter();

function Preson(){
  PresonCount.add();
}

//用Preson這個構造函數創建對象,每創建一次都相當於調用了一次該構造函數。
var p = new Preson()
var p1 = new Preson()
var p2 = new Preson()
var p3 = new Preson()
console.log(PresonCount.getCounts());        // 打印4

局部變量的累加,怎樣做到變量a即是局部變量又可以累加

// 1、全局變量
var a = 1
function abc(){
  a++
  console(a)
}

abc()  // 2
abc()  // 3
// 可以累加但問題是a是全局變量  容易被污染

// 2、局部變量
function abc () {
  var a = 1;
  a++;
  console(a);
}
abc() // 2
abc() // 2
// 放到局部裏又不能累加,因爲每次執行函數都相當於把a重新聲明

// 3、局部變量的累加
function outer () {
  var a = 1;
  return function () {
    a++;
    console.log(a);
  }
}

var y = outer();
y()  // 2
y()  // 3
// 這樣即實現了累加,又能把變量a藏起來。

模塊化代碼,減少全局變量的污染。a是局部變量,全局變量有a也沒關係

var abc = (function () {
  var a = 1;
  return function(){
    a++
    console(a)
  }
}());   // 函數在這裏自調用一次,所以abc得到的是abc裏返回的函數

abc();  // 2
abc();  // 3

函數的私有成員調用

var aaa = (function(){
  var a = 1;
  function bbb(){
    a++;
    console.log(a);
  }
  function ccc(){
    a++;
    console.log(a);
  }
  return {
    b:bbb,
    c:ccc     // json格式,也就是返回一個對象。b是bbb的函數體
  }
}());    // 自調用一下,這樣aaa就是函數體內的返回值,也就是那個json格式的對象

aaa.b();  //2
aaa.c();  //3

在循環中直接找到對應元素的索引

//這是以前的寫法
var lis = document.getElementsByTagName('li');
for (var i = 0; i < lis.length; i++) {
  lis[i].onclick = function(){
  console.log(i);   // 由於進入函數時i已經循環完畢,所以i變爲常量4
}

// 用閉包的方式來寫
for (var i = 0; i < lis.length; i++) {
  (function(i){
    lis[i].onclick = function(){
      console.log(i);    
    }
  }(i))  //在這裏調用一次,將i作爲參數傳進去,這時裏邊的i就不會是執行完之後的i值
}

內存泄漏問題

由於IE的js對象和DOM對象使用不同的垃圾收集方法,因此閉包在IE中會導致內存泄露問題,也就是無法銷燬駐留在內存中的元素

function closure(){
  var oDiv = document.getElementById('oDiv');    //用完之後會一直待在內存中
  var test = oDiv.innerHTML;
  oDiv.onclick = function () {
    alert(test);    // 這裏用oDiv導致內存泄漏
  };
  oDiv = null;    //最後應該將oDiv解除來避免內存泄漏
}

多閉包結構

像上邊的案例只需要一個n的值一個閉包就可以解決,而很多時候需要返回的變量大於1。

如下需要訪問函數內部的多個變量n和m,就需要多個閉包。閉包的實質就是一個函數。

function foo(){
  var n = 1,m = {age:20}; // n是變量,m是對象
  function getN(){
    return n;    
  }
  function getM(){
    return m;
  }
  return {getM:getM,getN:getN}; // :前的是屬性名,:後的是屬性值也就是函數體。
}

var obj = foo(); // 這就是一次閉包
obj.getM().age = 22;
console.log(obj.getM().age);    // 22
console.log(obj.getN());    // 1

var obj1 = foo(); // 這是第二次閉包,每閉包一次就是重新調用一次。不會被上次obj閉包調用並且更改屬性值而改變函數本身的值,這和原型的不可變特性比較像。

console.log(obj1.getM().age);    // 20

對象的私有屬性

// 用下面這個案例來說明構造函數的問題。
function Preson (name,age) {
  this.name = name;
  this.age = age;
}
// 這是創建對象並且傳參姓名
var xiaohong = new Preson("小紅",20)
// 這時如果一不小心,就能隨意將姓名改成小綠了。
xiaohong.name = "小綠"

// ---------------------------------------------------------------------------------------

// 爲了解決這個問題,可以用這種寫法
function Preson (name,age) {
  return {
    getName:function(){
        return name;
    },
    getAge:function(){
        return age;
    }
    // name通常不能更改,但是age 可以改,給了這樣一個接口就可以直接改了
    setAge:function(val){
        age = val;
    }
  }  
}
// 還是創建對象並且傳參
// 這樣就沒法隨意更改了,除非更改構造函數的函數。
var xiaohong = new Preson("小紅",20)

xiaohong.serAge(19);
xiaohong.getAge();     // 先傳一個參數19,讓age改爲19.再調用一下getAge函數。就將年齡屬性改爲了19


// 但是還有個問題,那就是通過下面的語句可以創建一個name的屬性。這樣也是不太好的
xiaohong.name = "小綠"
//通過下面這個屬性可以解決。但是要寫在上面創建屬性的語句的前面
Object.preventExtenions(xiaohong)
xiaohong.name = "小綠"  
console.log(xiaohong.name)  // 這時就返回undefined了。

用閉包來解決遞歸函數性能問題

 // 利用閉包可以緩存數據的特性,改善遞歸性能
 // 這個函數是爲了緩存
var fib = (function() {
  var cache = [];
  // 這個函數是求fib的第n項值
  return function(n) {
    if (n < 1) {
      return undefined;
    }
    // 1、看緩存裏有沒有
    // 如果有,直接返回值
    if (cache[n]) {
      return cache[n]
    } else
    // 如果沒有重新計算
    if (n === 1 || n === 2) {
      cache[n] = 1;
    } else {
      cache[n] = arguments.callee(n - 1) + arguments.callee(n - 2);
    }
    return cache[n];
  }
}())

console.log(fib(10));

垃圾回收機制

定義

GC(Garbage Collection),專門負責一些無效的變量所佔有的內存回收銷燬。

原理

垃圾收集器會定期(週期性)找出那些不在繼續使用的變量,然後釋放其內存。但這個過程不是實時的,因爲其開銷比較大,所以垃圾回收器會照固定的時間間隔週期性的執行。

爲什麼閉包會造成內存常駐,並且讓垃圾回收機制不能回收

不再使用的變量(生命週期結束的變量),當然只可能是局部變量,全局變量的生命週期直至瀏覽器卸載頁面纔會結束。局部變量只在函數的執行過程中存在,而在這個過程中會爲局部變量在棧或堆上分配相應的空間,以存儲它們的值,然後在函數中使用這些變量,直至函數結束,而閉包中由於內部函數的原因,外部函數並不能算是結束。

function fn1 () {
  // body...
  var obj = {
    name:'tom',
    age:20
  }
}

function fn2 () {
  // body...
  var obj = {
    name:'tom',
    age:20
  }
  return obj
}
var a = fn1();
var b = fn2();

當fn1被調用時,進入fn1環境,會開闢一塊內存存放對象obj,而當調用結束後,出了fn1的環境,那麼該塊內存會被js引擎中的垃圾回收器自動釋放,

而在fn2被調用的過程中,返回的對象被全局變量b所指向,所以該塊內存並不會被釋放。那麼問題出現了:到底哪個變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個變量沒用,對於不再有用的變量打上標記,以備將來收回其內存。用於標記的無用變量的策略可能因實現而有所區別,通常情況下有兩種實現方式:計數清除和引用清除。

引用計數法

跟蹤記錄每個值被引用的次數,如果一個變量被另外一個變量引用了, 那麼該變量的引用計數+1,如果同一個值又被賦值給另一個變量,則引用次數再+1。相反,當這個變量不再引用該變量時,這個變量的引用計數-1;GC會在一定時間間隔去查看每個變量的計數,如果爲0就說明沒有辦法再訪問這個值了就將其佔用的內存回收。

function test () {
  var a = {};   // a的引用次數爲0
  var b = a ;   // a的引用次數+1,爲1
  var c = a ;   // a的引用次數再+1, 爲2
  var b = {}    // a的引用次數減1,爲1
}

引用計數的缺點

function test () {
  var a = {};
  var b = {};
  a.pro = b;
  b.pro = a;
}

以上代碼a和b的引用次數都是2,fn()執行完畢後,兩個對象已經離開環境,在標記清除方式下是沒問題,但在引用計數策略下,因爲a和b的引用次數不爲0,所以不會被垃圾回收器回收內存,如果fn函數被大量調用,就會造成內存泄漏。只能手動讓a和b=null才能被識別並回收

window.οnlοad=function outerFunction(){
  var obj = document.getElementById("element");
  obj.οnclick=function innerFunction(){};
};

這段代碼看起來沒什麼問題,但是obj引用了document.getElementById("element")而document.getElementById("element")的onclick方法會引用外部環境值中的變量,自然也包括obj。
解決辦法:自己手工解除循環引用。

window.οnlοad=function outerFunction(){
  var obj = document.getElementById("element");
  obj.οnclick=function innerFunction(){};
  obj = null;
};

將變量設置爲null意味着切斷變量與它此前引用的值之間的連接。當垃圾回收器下次運行時,就會刪除這些值並回收它們佔用的內存。

標記清除法

從當前文檔的根部(window對象)找一條路徑,如果能到達該變量,那麼說明此變量有被其他變量引用,也就說明該變量不應該被回收掉,反之,應該被回收其所佔的內存

當變量進入某個執行環境(例如,在函數中聲明一個變量),那麼給其標記爲“進入環境”,此時不需要回收,但是如果上述執行環境執行完畢,便被銷燬,那麼該環境內的所有變量都被標記爲“已出環境”,如果被標記爲已出環境,就會被回收掉其佔用的內存空間。

function test() {
  var a = 10;  // 被標記,進入環境
  var b = 20;  // 被標記,進入環境
}
test()   // 執行完畢後,a,b被標記離開環境,被回收。

垃圾回收器在運行時會給存儲在內存中的所有變量都加上標記,然後,它會去掉環境中的變量以及環境中的變量引用的變量的標記(閉包)。而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最後,垃圾回收器完成內存清除工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。目前IE,Firefox,Opera,Chrome,Safari的js實現使用的都是標記清除的垃圾回收策略,只不過時間間隔不相同。

沙箱

變量不寫在全局上,但又想達到寫在全局的目的,就用沙箱

特點:

  1. 能分割作用域,不會污染全局(函數)

  2. 在分割後的作用域的內部的代碼要自執行。(匿名函數)

// 結構:
(function(){
  //代碼塊
}());

// 經典的沙箱模式:
var n = 2
(function  () {
  // 這個n不會污染外部的n。所以這樣能保證自己的代碼安全執行(別人也污染不了我),也不會污染全局變量或其他作用域的變量
  var n = 1;
  function foo () {
    console.log(n);
  }
  //window.fn 相當於設定了一個全局變量
  window.fn = foo;
}())
fn();
發佈了39 篇原創文章 · 獲贊 1 · 訪問量 8335
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章