JS高級(三)--原型鏈、閉包、作用域、函數的四種調用方式


一、原型鏈(家族族譜)


 

  • 概念:JS里面的對象可能會有父對象,父對象還會有父對象,。。。。。祖先

  • 根本:繼承

屬性:對象中幾乎都會有一個__proto__屬性,指向他的父對象

意義:可以實現讓該對象訪問到父對象中相關屬性

  • 根對象:Object.prototype

var arr=[1,3,5]

arr.__proto__:Array.prototype

arr.__proto__.__proto__就是找到了根對象

function Animal(){}
var cat=new Animal();
//cat.__proto__:Animal.prototype
//cat.__proto__.__proto__:根對象
  • 錯誤的理解:在js中,萬物繼承自Object? -->在js中萬物繼承自Object.prototype

二、作用域


(一)、變量作用域 

  • 變量作用域的概念:就是一個變量可以使用的范圍

  • JS中首先有一個最外層的作用域:稱之為全局作用域

  • JS中還可以通過函數創建出一個獨立的作用域,其中函數可以嵌套,所以作用域也可以嵌套

var age=18; //age是在全局作用域中聲明的變量:全局變量
function f1(){
    console.log(name); //可以訪問到name變量
    var name="周董" //name是f1函數內部聲明的變量,所以name變量的作用域就是在f1函數內部
    console.log(name); //可以訪問到name變量
    console.log(age); //age是全局作用域中聲明的,所以age也可以訪問
}
console.log(age); //也可以訪問

 

//-->1級作用域
var gender="男";
function fn(){
    console.log(age);    //因為age是在fn作用域內聲明的
    //age:undefined:既然有值就是可以訪問
    console.log(height);//height不是在該作用域內部聲明的,所以不能訪問
    //-->2級作用域
    return function(){
        //-->3級作用域
        var height=180;
    }
    var age=5;
}

//注意:變量的聲明和賦值是在兩個不同時期的
function fn(){
    console.log(age);   //undeinfed
    var age=18;
    console.log(age);   //18
}

//fn函數執行的時候,首先找到函數內部所有的變量、函數聲明,把他們放在作用域中,給變量一個初始值:undefined    -->變量可以訪問
//逐條執行代碼,在執行代碼的過程中,如果有賦值語句,對變量進行賦值

function fn(){
    var age;        //初始值:undefined
    console.log(age);   //undeinfed
    age=18;     //修改了變量的值
    console.log(age);   //18
}

 

(二)、作用域鏈

  • 由於作用域是相對於變量而言的,而如果存在多級作用域,這個變量又來自於哪里?這個問題就需要好好地探究一下了,我們把這個變量的查找過程稱之為變量的作用域鏈

  • 作用域鏈的意義:查找變量(確定變量來自於哪里,變量是否可以訪問)

  • 簡單來說,作用域鏈可以用以下幾句話來概括:(或者說:確定一個變量來自於哪個作用域)

  1. 查看當前作用域,如果當前作用域聲明了這個變量,就確定結果

  2. 查找當前作用域的上級作用域,也就是當前函數的上級函數,看看上級函數中有沒有聲明

  3. 再查找上級函數的上級函數,直到全局作用域為止

  4. 如果全局作用域中也沒有,我們就認為這個變量未聲明(xxx is not defined)

function fn(callback){
    var age=18;
    callback()
}

fn(function(){
    console.log(age);
    //分析:age變量:
    //1、查找當前作用域:並沒有
    //2、查找上一級作用域:全局作用域
    //-->難點:看上一級作用域,不是看函數在哪里調用,而是看函數在哪里編寫
    //-->因為這種特別,我們通常會把作用域說成是:詞法作用域
})

舉例1:

    var name="張三";
    function f1(){
        var name="abc";
        console.log(name);  //abc
    }
    f1();

舉例2:

    var name="張三";
    function f1(){
        console.log(name);  //underfind
        var name="abc";
    }
    f1();

舉例3:

    var name="張三";
    function f1(){
        return function(){
            console.log(name); //underfind
        }
        var name="abc";
    }
    var fn=f1();
    fn();

舉例4:

    var name="張三";
    function f1(){
        return {
            say:function(){
                console.log(name);  //underfind
                var name="abc";
            }
        }
    }
    var fn=f1();
    fn.say()

三、閉包 


 

(一)、認識閉包

    var divs=document.getElementsByTagName("div");
    for (var i = 0; i < divs.length; i++) {
        const element = divs[i];
        element.onclick=function(){
            //i是來自於全局作用域
            alert(i)
        }
    }
    // 執行完for循環之后,i的值已經變成了5

閉包的解決

    var divs=document.getElementsByTagName("div"); 
    for (var i = 0; i < divs.length; i++) {
        const element = divs[i];
        //閉包的解決方案
        element.onclick=(function(j){
            return function(){
                //i是來自於全局作用域
                alert(j)
            }
        })(i);
    }   

例一:

    function fn(){
        var a=5;
        return function(){
            a++;
            console.log(a);     //a變量肯定是可以訪問的
        }
    }
    var f1=fn();        //f1指向匿名函數
    f1();       //6
    f1();       //7
    f1();       //8
    //代碼執行到8行fn函數執行完畢,返回匿名函數
    //      -->一般認為函數執行完畢,變量就會釋放,但是此時由於js引擎發現匿名函數要使用a變量,所以a變量並不能得到釋放,而是把a變量放在匿名函數可以訪問到的地方去了
    //      -->a變量存在於f1函數可以訪問到的地方,當然此時a變量只能被f1函數訪問  

例二:

    function fn(){
        var a=5;
        return function(){
            a++;
            console.log(a);     //a變量肯定是可以訪問的
        }
    }
    var f1=fn();        //f1指向匿名函數
    f1();       //6
    f1();       //7
    f1();       //8
    //把a變量的值放在f1函數可以訪問到的地方

    var f2=fn();
    f2();       //6
    f2();       //7
    f2();       //8
    //又一次執行了fn,又初始化了一個新的a變量,值為5;返回匿名函數f2,並且把新的a變量放在了f2可以訪問到的地方

    var f3=fn();
    f3();       //6
    //又一次執行了fn,又初始化了一個新的a變量,值為5;返回匿名函數f2,並且把新的a變量放在了f2可以訪問到的地方 

例三:

    function q1(){
        var a={};
        return a;
    }
    var r1=q1();
    var r2=q1();
    console.log(r1==r2);  //fasle


    function q2(){
        var a={}
        return function(){         
            return a;
        }
    }
    var t3=q2();//創建一個新的a對象,把a對象放在t3可以訪問到的位置
    var o5=t3();    //返回值a就是那個a
    
    var w3=q2();//創建了一個新的a對象,把新的a對象放在w3可以訪問到的位置
    var o8=w3();//此時獲取到的是一個新的a對象
    console.log(o5==o8);    //false 

(二)、閉包的應用場景

  • 模塊化

  • 防止變量被破壞

    //模塊化思想:也是一種設計模式
    var ktv=(function KTV(){
        //為了保護leastPrice變量,將它放在函數內部
        var leastPrice=1000;
        var total=0;
        return {
            //購物
            buy:function(price){
                total+=price;
            },
            //結賬
            pay:function(){
                if(total<leastPrice){
                    console.log('請繼續購物');
                }else{
                    console.log('歡迎下次光臨');
                }
            },
            editLeast:function(id,price){
                if(id===888){
                    leastPrice=price;
                    console.log("現在最低消費金額為:",leastPrice);
                }else{
                    console.log('權限不足');
                }
            }
        }
    })()

    //假設:來了朋友要來唱K
    //——>可能老板需要去修改最低消費的金額
    //-->但是並不能讓老板直接去修改leastPrice,或者說不能把leastPrice作為全局變量

(三)、閉包問題的產生原因 

  • 函數執行完畢后,作用域中保留了最新的a變量的值 
    function f1(){
        var a=5;
        return function(){
            a++;
            console.log(a);
        }
    }
    var q1=f1();
    
    //要想釋放q1里面保存的a,只能通過釋放q1
    q1=null;    //q1=undefined 

四、函數的四種調用方式


  •  在`ES6之前`,函數內部的this是由該函數的調用方式決定的
  • 在ES6的箭頭函數之前的時代,想要判斷一個函數內部的this指向誰,就是根據下面的四種方式來決定的
  • 函數內部的this跟大小寫、書寫位置無關

(一)、函數調用

  • 函數內部的this指向window 

例一:

    var age=18;
    var p={
        age:15,
        say:function(){
            console.log(this.age); //18
        }
    }
    var f1=p.say;   //f1是函數
    f1();   //函數調用-->this:window       -->this.age=18  

例二:

    function Person(name) {
      this.name = name;
    }
    Person.prototype = {
      constructor: Person,
      say: function () {
        console.log(this.name);  //abc
      }
    } 
    //函數的第一種調用方式:函數調用 
    // -->函數內部的this指向window 
    // -->window對象中的方法都是全局函數,window對象中的屬性都是全局變量 
    Person("abc");
    var p1=Person.prototype.say
    p1()  

例三:

    function fn() {
      console.log(this.age)  //undefined
    }
    fn(); 

(二)、方法調用

  • 函數內部的this指向調用該方法的對象

例一:

    function Person() {
      this.age = 20;
    }
    Person.prototype.run = function () {
      console.log(this.age);
    }
    var p1 = new Person();
    p1.run();
    //打印結果:20  

 例二:

    var p2={
        height:180,
        travel:function(){
            console.log(this.height);
        }
    }
    p2.travel()     //打印結果:180

例三:

    var clear=function(){
        console.log(this.length);
    }    
    var length=50;
    var tom={ 
      c:clear,
      length:100 
    };
    tom.c();        //這里是方法調用的方式        
    //打印this.length 是50 還是100?
    //-->相當於:this是指向window還是指向tom呢? 
    //  -->結果為:100  
    //      -->this:tom
    //結論:由於clear函數被當成tom.c()這種方法的形式來進行調用,所以函數內部的this指向調用該方法的對象:tom 

    var tony={ d:clear,length:30 };
    tony.d();       
    //方法調用的方式,所以clear函數內部的this指向tony的,

(三)、構造函數的調用方式(new調用)

  • this指向構造函數的實例

例一:

    function fn(name){
        this.name=name;
    }
    //通過new關鍵字來調用的,那么這種方式就是構造函數的構造函數的調用方式,那么函數內部的this就是該構造函數的實例
    var _n=new fn("小明");  //_n有個name屬性,值為:小明  

例二:

    function jQuery(){
        var _init=jQuery.prototype.init;
        //_init就是一個構造函數
        return new _init();
    }
    jQuery.prototype={
        constructor:jQuery,
        length:100,
        init:function(){
            //this可以訪問到實例本身的屬性,也可以訪問到init.prototype中的屬性
            //這里的init.prototype並不是jQuery.prototype
            console.log(this.length);   
            //正確答案:undefined
            //變量聲明了未賦值才是undefind、屬性不存在也是undefined
        }
    } 

例三:

    function jQuery(){
        var _init=jQuery.prototype.init;
        //_init就是一個構造函數
        return new _init();
    }
    jQuery.prototype={
        constructor:jQuery,
        length:100,
        init:function(){
            //this指向init構造函數的實例
            //-->1、首先查看本身有沒有length屬性
            //-->2、如果本身沒有該屬性,那么去它的原型對象中查找
            //-->3、如果原型對象中沒有,那么就去原型對象的原型對象中查找,最終一直找到根對象(Object.prototype)
            //-->4、最終都沒有找到的話,我們認為該對象並沒有該屬性,如果獲取該屬性的值:undefined
            console.log(this.length);   //100   
        }
    }
    var $init=jQuery.prototype.init;
    //修改了init函數的默認原型,指向新原型
    $init.prototype=jQuery.prototype;
    jQuery();

(四)、上下文調用方式(call、apply、bind)

  • call方法

call方法的第一個參數:

1、如果是一個對象類型,那么函數內部的this指向該對象

2、如果是undefined、null,那么函數內部的this指向window

3、如果是數字-->this:對應的Number構造函數的實例

--> 1 --> new Number(1)

4、如果是字符串-->this:String構造函數的實例

--> "abc" --> new String("abc")

5、如果是布爾值-->this:Boolean構造函數的實例

--> false --> new Boolean(false)

    function f1(){
        console.log(this);
    }
    //call方法的第一個參數決定了函數內部的this的值
    f1.call([1,3,5])
    f1.call({age:20,height:1000})
    f1.call(1)      
    f1.call("abc")
    f1.call(true);
    f1.call(null)
    f1.call(undefined);  
  • apply方法 

1、上述call的代碼apply完全可以替換

2、call和apply都可以改變函數內部的this的值

3、不同的地方:傳參的形式不同

    function toString(a,b,c){
        console.log(a+" "+b+" "+c);
    }
    toString.call(null,1,3,5)   //"1 3 5"
    toString.apply(null,[1,3,5])//"1 3 5"  
  • bind方法 

bind是es5中才有的(IE9+)

例一:

    var obj = {
        age:18,
        run : function(){
            console.log(this);  //this:obj         
            var _that=this;
            setTimeout(function(){
                //this指向window
                console.log(this.age); 
                console.log(_that.age); 
                //undefined是正確的
            },50);
        }
    } 

例二:

    var obj5 = {
        age:18,
        run : function(){
            console.log(this);  //this:obj5
            setTimeout((function(){
                console.log(this.age); 
            }).bind(this),50);  //this:obj5
            //通過執行了bind方法,匿名函數本身並沒有執行,只是改變了該函數內部的this的值,指向obj5
        }
    }
    obj5.run(); 

例三:

    //bind基本用法
    function speed(){
        console.log(this.seconds);
    }
    //執行了bind方法之后,產生了一個新函數,這個新函數里面的邏輯和原來還是一樣的,唯一的不同是this指向{ seconds:100 }
    var speedBind = speed.bind({ seconds:100 });
    speedBind();    //100  

例四:

    (function eat(){
        console.log(this.seconds);
    }).bind({ seconds:360 })()  //360

例五:

    var obj={
        name:"西瓜",
        drink:(function(){
            //this指向了:{ name:"橙汁" }
            console.log(this.name);
        }).bind({ name:"橙汁" })
    }
    obj.drink();    //"橙汁"

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM