JS 高級(函數、作用域、閉包、this、垃圾回收)

JS 函數

函數分為兩類具名函數、匿名函數,其變型可以包括自執行函數、遞歸函數

  1. 具名函數

    含有名字的函數

    function say(){//...}定義了一個名稱為 say 的函數
    say();//調用名稱為 say 的函數
    var say = function(){//...}
    var say1 = function say(){//...}//不建議這樣寫
    
  2. 匿名函數

    不含名字的函數

    setTimeout(function(){
       
    },100);
    li.onclick = function(){}
    
  3. 自執行函數

    創建即執行的函數,可以創建塊級作用域

    (function(){
      //..
    })();
    
  4. 遞歸函數

    自己在某些條件下調用自己的函數

    function calc(num){
      if(num<1){
        return 1;
      }else{
        return num*calc(num-1);
      }
    }
    console.log(calc(4));//24 一個典型的階乘遞歸  1*2*3*4
    

    ?

JS 作用域

概念:當某個函數被調用時,會創建一個執行環境及相應的作用域(鏈),該執行環境定論了變量或函數有權訪問的其它數據,決定了各自的行為

每一個執行環境都有一個變量對象[[Scope]],該對象保存著執行環境中的所有變量和函數,我們的代碼無法訪問這個對象,JS 解析器在處理數據時會使用到它。

全局執行環境

在 JS 中,根據宿主的不同,全局執行環境對象也不同,對于 WEB 而言,全局執行環境是 window 對象。

所有的全局變量和函數都是做為 window 對象的屬性和方法創建的。

var name = "zhar";
function say(){
  console.log(name);
}
console.log(name);//zhar
console.log(window.name);//zhar
say();//zhar
window.say();//zhar

局部執行環境

每個函數有自己的執行環境。

當執行流進入一個函數時,函數的環境會被推入一個環境棧中。函數執行完成后,棧將環境推出。

作用域鏈

當代碼在一個環境中執行時,會創建一個作用域鏈對象(scope chain)

作用域鏈的首位,始終是當前執行環境的變量對象。這是一個包含 arguments 和其它命名參數值的活動對象。第二位是外部函數的活動對象,第三位是外部函數的外部函數的活動對象,......,直到作為作用域終點的全局執行環境。

var name = "zhar";
function say(){
  var age = 30;
  console.log(name+"-"+age+"-"+address);// 錯誤:address is not defined
  function info(){
    var address = "北京";
    console.log(name+"-"+age+"-"+address);//zhar-30-北京
  }
  info()
}
say();//
console.log(name+"-"+age+"-"+address);// 錯誤:age is not defined

這段代碼共有三個執行環境:全局環境(window)、say()局部環境、info()局部環境。

在全局環境中有一個變量 name 和一個函數 say,在 say 中有一個變量 age 和一個函數 info,在 info 中有一個變量 address。通過報錯信息可以看到,say 中可以訪問到當前環境中的 age 及全局環境中的 name,info 可以訪問到當前環境中的 address、say 環境中的 age、全局環境中的 name,而在 window 環境中則只可以訪問到 name。

可以得出:內部環境可以通過作用域鏈訪問到所有的外部環境,但外部環境不能訪問內部環境中的任何變量或函數。

作用域鏈是有層級、線性的;

JS 在查找變量時,在當前環境查找到變量便停止繼續向上搜索

var name = 'zhar';
function say(){
  var name = 'tom';
  console.log(name);//tom
}
say();
console.log(name);//zhar

作用域鏈示意圖:

沒有塊級作用域

在 JS 中并沒有像強類型語言中的塊級作用域,比如在 JAVA 中一對花括號中,是一個塊級的作用域

var name = 'zhar'
if(true){
  var name = 'tom';
}
console.log(name);
//按著上面作用域鏈中描述的情況,name 應該輸出為 zhar,但實際情況為 tom,便是因為在 JS 中沒有塊級作用域的概念
//另外一種塊級作用域的典型情況為 for 循環
for(var i=0;i<5;i++){
  //dosomething
}
console.log(i);//5

變量提升

var name = 'zhar';
function say(){
  console.log(name);//undefined
  var name = 'tom';
}
say();

上面的示例代碼中就存在著變量提升

  1. 聲明式

    say();//運行正常
    function say(){
      console.log("Hello");
    }
    

    此為聲明式語句,JS 解析器會將聲明式語句提升至當前執行環境的頂端

  2. 賦值式

    將上面的代碼更改如下:

    say();//運行錯誤  Uncaught TypeError: say is not a function
    var say = function(){
      console.log("Hello");
    }
    

    此為賦值式語句,JS 解析器會將賦值式語句提升到當前執行環境的頂端,并且賦值為 undefined

    更加直接的例子便是該知識點開始的代碼

JS閉包

概念:外部函數返回的,持有外部函數局部變量的內部函數

(有權訪問另一個函數作用域中的變量的函數)

換句話說,JS 中任意一個函數都能成為閉包函數

創建閉包

通常就是在一個函數內部創建另一個(匿名)函數

function say(){
  var name = "zhar";
  return function(){
    return name+"-30";
  }
}
//獲取閉包
var hi = say();//Function
console.log(hi());//zhar-30

上面示例中,第三行代碼做為 say 方法的一個內部函數,訪問了外部的 name 屬性,該函數被返回,在其它地方被調用時,仍然可以訪問到 say 方法內的 name 屬性

根據前面所學的作用域鏈的知識,可以得出匿名函數中 包含了外部作用域中的 name 屬性的引用,當匿名函數被返回后,它的作用域鏈包含上級函數(say)的活動對象(name)。這樣,當 say 函數執行完成后,其活動對象也并不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。

由于閉包所引用的變量不會被自動銷毀的特性,在使用閉包時要非常小心,有可能會引起內存占用過多

閉包常見場景

  1. 循環添加事件 BUG

    var lis = document.querySelectorAll("li");
    for(var i=0;i<lis.length;i++){
      lis[i].onclick = function(){
        console.log(this.innerHTML,i);
      }
    }
    //在上面的代碼中,innerHTML能夠得到正確的值,但 i 并不能,這是由于在 onclick 所指定的匿名函數中使用了外部環境中的同一個活動對象 "i",當外部環境執行完成后,i 的值是 lis 的長度,所以引用了所有 i 的對象值都變為了 lis的長度
    //修改代碼如下:
    for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = (function(index) {
            return function() {
                console.log(lis[index].innerHTML, index);
            }
        })(i);
    }
    //修改后的代碼將 onclick 指定的匿名函數改為了立即執行,并將外部環境的 i 值做為參數傳遞給該函數(參數是做為值傳遞),而在匿名函數內部,由一個閉包函數來引用 index
    
    //另一種方法(這種方法只是做為一種擴展,與本知識點無關)
    for (var i = 0; i < lis.length; i++) {
        lis[i].index = i;
        lis[i].onclick = function() {
            console.log(this.innerHTML, this.index);
        };
    }
    
  2. 使用閉包封裝一個完整的對象操縱功能

    對于一些封裝,開發者并不想對外暴露一些屬性或變量,此時,可通過閉包來創建

    var PERSON = function() {
        var obj = {
            name: "zhar",
            age: 30,
            address: "北京"
        };
        return {
            get: function(pName) {
                return obj[pName];
            },
            add: function(pName, pVal) {
                obj[pName] = pVal;
            },
            del: function(pName) {
                delete obj[pName];
            },
            update: function(pName, pVal) {
                obj[pName] = pVal;
            }
        }
    }();
    console.log(PERSON.get("name"));//zhar
    PERSON.add("desc","175");
    console.log(PERSON.get("desc"));//175
    PERSON.del("age");
    console.log(PERSON.get("age"));//undefined
    

關于作用域、閉包的一些經典題目

function f1() {
    var n = 999;
    nAdd = function() {
        n += 1
    }
    function f2() {
        console.log(n);
    }
    return f2;
}
var result = f1();
result();
nAdd();
result();
function show(i) {
    var a = i;
    function inner() {
        a = 10;
        console.log(1,a);
    }
    inner();
    console.log(2,a);
}
var a = 0;
show(5);
console.log(3,a);
var too = "old";
if (true) {
    var too = "new";
}
console.log(1,too);
function test() {
    var too = "new";
}
test()
console.log(2,too);
var i = 0;
function outPut(i) {
    console.log(i)
}
function outer() {
    outPut(i);
    function inner() {
        outPut(i);
        var i = 1;
        outPut(i);
    }
    inner();
    outPut(i);
}
outer();
function fun(n, o) {
    console.log(o)
    return {
        fun: function(m) {
            return fun(m, n);
        }
    };
}
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
var a = function(){
    a=2;
    console.log(1111,a);
}
function a(){
    a=1;
    console.log(2222,a);
}
a();
a();

this

this 是 JS 中的一個關鍵字,代表了函數運行時,自動生成的一個內部對象,只能在函數內部使用

我們要討論的是 this 的指向

this 的指向并不能在創建是就決定了,而是由執行環境決定的

this 指向

全局環境(純粹的函數調用)

在全局環境下,this 就代表 window(這是針對 WEB 應用來講的)

var name = 'zhar';
function say(){
  console.log(this.name);//zhar
}
say();

同樣,在 setTimeout 或 setInterval 這樣的延時函數中調用也屬于全局對象

var name = 'zhar';
setTimeout(function(){
  console.log(this.name);//zhar
},0);

對象環境

var obj = {
  name : "zhar",
  say : function(){
    console.log(this.name);//zhar
  }
}
obj.say();

如上面的代碼所示,函數被其所必對象調用,那么 this 便指向 obj 對象

觀察下面的代碼

var name = 'tom';
var obj = {
  name : "zhar",
  say : function(){
    console.log(this.name);
  }
}
var fun = obj.say;
fun();//輸出 ?

另外一種情況:

var name = 'tom';
var obj = {
  name : "zhar",
  say : function(){
    return function(){
      console.log(this.name);
    }
  }
}
obj.say()();//輸出 ?

構造函數環境

構造函數中 this 會指向創建出來的一個對象,使用new調用構造函數時,會先創建出一個空對象,然后調用call函數把構造函數中的指針修改為指向這個空對象,執行完構造函數后,這個空對象也就有了相關的屬性方法并返回這個對象。

function Person() {
    this.name = 'zhar';
}
var p = new Person();
console.log(p.name);

構造函數不需要返回值,如果指定返回值要小心,指定返回一個對象,則 this 的指向將發生變化

function Person() {
  this.name = 'zhar';
  return {};
}
var p = new Person();
console.log(p.name);//undefined
//--------------------------------------
function Person() {
  this.name = 'zhar';
  return {name:'tom'};
}
var p = new Person();
console.log(p.name);//tom      如果構造函數返回對象(Object,Array,Function),那 this 將指向這個對象,其它基礎類型則不受影響
//--------------------------------------
function Person() {
  this.name = 'zhar';
  return 1;//number string boolean 等
}
var p = new Person();
console.log(p.name);//zhar

事件環境

在 DOM 事件中使用 this,this 指向了觸發事件的 DOM 元素本身

li.onclick = function(){
    console.log(this.innerHTML);
}

更改 this 指向

  1. 使用局部變量替代

    var name = "zhar";
    var obj = {
      name : "zhar",
      say : function(){
        var _this = this;//使用一個變量指向 this
        setTimeout(function(){
          console.log(_this.name);
        },0);
      }
    }
    obj.say();
    

    ?

  2. 使用 call 或 apply 方法

    call 是函數的一個方法,MDN上的官方定義為:call方法調用一個函數, 其具有一個指定的this值和分別提供的參數

    語法:

    fun.call(thisObj[,arg1[,arg2[,...]]])

    通俗來講:call 用來更改 this 的指向

    通過一系列代碼來展示 call 的用法:

    var name = 'zhar';
    function say(){
      console.log(this.name);
    };
    say();//zhar;
    var obj = {
      name : 'tom',
      say : function(){
        console.log(this.name);
      }
    }
    say.call(obj);//tom      將 say 函數中的 this 替換為傳入的對象
    obj.say();//tom
    obj.say.call(null);//zhar    將 obj.say 函數的 this 替換為了 null,也就意味著指向了全局環境
    
    //前面課程的繼承代碼
    function Person(){
      this.name = "人";
    }
    function Student(){
      Person.call(this,null);
    }
    var s = new Student();
    console.log(s.name);
    
    li.onclick = function(){
      console.log(this.innerHTML);//此處的 this 代表著 DOM 元素
      function update(){
        this.innerHTML += " new ";
      }
      //update();//這樣做的話,this 的指向將變為window
      update.call(this);//通過 call 方法修改函數內 this 的指向
    }
    
    //call 的傳參
    function say(arg1,arg2){
      console.log(this.name,arg1,arg2);
    };
    var obj = {
      name : 'tom',
      say : function(){
        console.log(this.name);
      }
    }
    say.call(obj,'one','two');//tom one two
    

    ?

    apply

    applay 與 call 的作用相同,不同之處在于傳參的形式,apply 是以數組的形式傳遞參數的,而 call 方法的參數可以是任意類型

    //apply 的傳參
    function say(arg1,arg2){
      console.log(this.name,arg1,arg2);
    };
    var obj = {
      name : 'tom',
      say : function(){
        console.log(this.name);
      }
    }
    say.apply(obj,['one','two']);//tom one two
    

堆棧

理解了堆棧內存才會對值引用和地址引用有更好的理解

棧(stack)和堆(heap)

stack 為自動分配的空間,它由系統自動釋放

heap 為動態分配的內存,大小不定也不會自動釋放

基本數據類型存放于棧內存中,Undefined Null String Number Boolean,它們是直接按值存放的,可以直接訪問

引用數據類型存放于堆內存中,變量只是保存的一個指針,該指針指向堆內存中的地址,當訪問引用類型數據(Array、Object、Function等)時,先從棧中獲得該對象的指針,再從堆中取出對象的數據

值傳遞與地址傳遞

var a = 10;
var b = a;
b = 20;
console.log(a,b);
//以上的代碼修改 b 的值并不會影響 a 的值

var a = [1,2,3,4];
var b = a;
var c = a[0];
console.log(b);
console.log(c);
b[0] = 9;
c = 10;
console.log(a);//[9,2,3,4]
//以上代碼可以看出 當改變 b 中的數據時,a 中的數據也發生了變化;改變 c 的 數據時,a 不會受影響
//a 是數組,屬引用類型,當將 a 賦值給 a 時,傳遞的是棧中的地址,而不是堆內存中的對象。
//而 c 只是從 a 堆內存中獲取的一個數據值,保存于棧中。修改 c 時,是在棧中直接修改

淺拷貝與深拷貝

在定義引用數據類型時,變量存放的只是一個地址。當使用對象拷貝,傳遞的也只是一個地址。因此在訪問拷貝對象屬性時,會根據地址找到源對象指向的堆內存中。

?

淺拷貝

var obj1 = {
    name : "zhar",
    desc : ["北京"]
}
function copy(o1){
    var newObj = {};
    for(var key in o1){
        newObj[key] = o1[key];
    }
    return newObj;
}
// var obj2 = obj1;
var obj2 = copy(obj1);
obj2.name = "tom";
obj2.desc.push("昌平");
console.log(obj1);//{ name: 'zhar', desc: [ '北京', '昌平' ] }

深拷貝

var obj1 = {
    name : "zhar",
    desc : ["北京"]
}
function copy(obj,target){
    var newObj = target || {};
    console.log(obj)
    for(var key in obj){
        if(typeof obj[key] === "object"){
            newObj[key] = (obj[key].constructor===Array)?[]:{};
            copy(obj[key],newObj[key]);
        }else{
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
var obj2 = copy(obj1);

垃圾回收

Javascript 具有自動垃圾回收機制,執行環境會管理代碼執行過程中使用的內存。

使我們不必像 C 或 C++開發者那樣,手動去管理內存的釋放。

自動回收機制原理:

垃圾回收器按照固定的時間間隔找出不再繼續使用的變量,然后釋放其占用的內存。

兩種策略:

  1. 標記清除

    最常用的垃圾回收方式。當變量進入環境時,將變量標記為"進入環境";當變量離開環境時,將其標記為"離開環境"。

    到2008年,各瀏覽器使用的清除策略都是標記清除,差別在于時間間隔不同

  2. 引用計數

    引用計數是跟蹤每個值被引用的次數;當有一個變量被引用時,則這個值的引用次數加1,當取消一個引用時,次數減1。當引用值變為0 時,將由垃圾收集器回收

    引用計數方式有嚴重的問題,就是,當變量相互引用時,如:

    var obj1 = {}
    var obj2 = {}
    obj1.a = obj2;
    obj2.b = obj1;
    

    對于上面的代碼,存在相互引用,其引用計數永遠為2,就會導致對象永遠不會被回收。

    obj = null;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容