閉包、原型鏈和繼承

閉包、原型鏈和繼承
 
 
閉包(closure)
 
閉包的概念
官方對閉包的解釋是:一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。
 
閉包是指有權訪問另外一個函數做用域中的變量的函數
閉包就是可以讀取其餘函數內部變量的函數
閉包能夠理解成定義在一個函數內部的函數
函數就是閉包
 
當一個函數可以記住並訪問到其所在的詞法做用域及做用域鏈,特別強調是在其定義的做用域外進行的訪問,此時該函數和其上層執行上下文共同構成閉包。
 
須要明確的幾點:
1. 閉包必定是函數對象
2. 函數內保持對上層做用域的引用
3. 閉包和詞法做用域、做用域鏈、垃圾回收機制等息息相關
4. 當函數在其定義的做用域外進行訪問時,才產生閉包
5. 閉包是由該函數和其上層執行上下文共同構成
 
變量及做用域:
變量無非就是兩種:全局變量和局部變量。
Javascript語言中,函數內部能夠直接讀取全局變量,在函數外部沒法直接讀取函數內的局部變量。
 
程序設計中做用域的概念:
一般來講,一段程序代碼中所用到的名字並不老是有效/可用的,而限定這個名字的可用性的代碼範圍就是這個名字的做用域。
 
詞法做用域:
詞法做用域,也叫靜態做用域,它的做用域是指在詞法分析階段就肯定了,不會改變。
動態做用域,是在運行時根據程序的流程信息來動態肯定的,而不是在寫代碼時進行靜態肯定的。
主要區別:詞法做用域是在寫代碼或者定義時肯定的,而動態做用域是在運行時肯定的。
詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用。
 
javascript 使用的是詞法做用域
// 詞法做用域
var abc = 1;
function f1() {
    console.log(abc);
}
function f2() {
    var abc = 2;
    f1();
}
f2();
 
// 相似動態做用域
function show() {
    console.log(this);
}
show();
document.querySeletor(".btn").onclick = function () {
    console.log(this);
    show();
}
document.querySelector(".btn").onclick = show;
var timer=setTimeout(show,1000);
 
做用域鏈
做用域鏈:本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
 
每一個執行環境都有一個與之關聯的變量對象,執行環境中定義的全部變量和函數都保存在這個變量對象中。
 
全局執行環境是最外圍的一個執行環境,在Web瀏覽器中,全局執行環境的變量對象是window對象。
當JavaScript解釋器初始化執行代碼時,首先默認進入全局執行環境。
 
局部執行環境的變量對象,則只在函數執行的過程當中存在。
當函數被調用的時候,會建立一個特殊的對象–活動對象。
活動對象以後會做爲局部執行環境的變量對象來使用。
 
function compare(value1,value2){
    if(value1 < value2){
        return -1;
    } else if( value1 > value2 ) {
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);
 
 
垃圾回收機制
各大瀏覽器一般採用的垃圾回收有兩種方法:標記清除、引用計數
 
標記清除
當變量進入執行環境時,將這個變量標記爲「進入環境」。當變量離開執行環境時,則將其標記爲「離開環境」,就銷燬回收內存。
 
引用計數
跟蹤記錄每一個值被引用的次數,當引用次數變成0時,就銷燬回收內存
 
function fn1(){
    var n = 5;
    n++;
    return n;
}
console.log( fn1() ); 
console.log( fn1() ); 
 
閉包的做用
閉包最大用處有兩個:在函數外能夠讀取函數內部的變量;讓這些變量的值始終保持在內存中。
 
function fn1(){
    var n = 5;
    return function fn2() {
        n++;
        return n;
    }
}
var fn = fn1();
console.log( fn() );
console.log( fn() );
console.log( fn() );
 
注意:
閉包會使得函數中的變量被保存在內存中,增長內存消耗,不能濫用閉包,不然會形成網頁的性能問題,在低版本IE中還可能致使內存泄露。
 
 
原型及原型鏈
 
JavaScript一種直譯式腳本語言,是一種動態類型、弱類型、基於原型的語言。
在全部語言中,JavaScript 幾乎是獨一無二的,也許是惟一的能夠被稱爲「面向對象」的語言,
由於能夠根本沒有類而直接建立對象的語言不多,而 JavaScript 就是其中之一。
 
在 JavaScript 中,類不能(由於根本不存在)描述對象能夠作什麼,對象直接定義它本身的行爲。
 
JavaScript 只有 對象。
咱們把JS對象分爲 普通對象 和 函數對象
 
屬性:prototype(原型)
每一個函數對象(Function.prototype除外)都有一個prototype屬性,這個屬性指向一個對象即 原型對象
var fn1 = function (){ };
var fn2 = new Function();
function fn3(){ };
console.log(fn1.prototype);
console.log(fn2.prototype);
console.log(fn3.prototype); // Object{} 這就是咱們所說的原型,它是一個對象也叫原型對象
 
// 爲何說 Function.prototype 除外呢?看代碼:
console.log(Number.prototype);
console.log(String.prototype);
console.log(Function.prototype);
console.log(Function.prototype.prototype); // 結果看下圖
能夠看到內置構造函數Number、String等,它們的原型指向一個普通對象(Number{}和String{})
而Function的原型則指向函數對象 function () { [native code] },就是原生代碼,二進制編譯的!
這個函數對象(Function.prototype)是沒有原型屬性的,因此它的prototype返回 undefined。
 
咱們繼續來了解
function Cat(){};
Cat.prototype.name = '小白'; // 給原型對象添加屬性
Cat.prototype.color = 'black'; // 給原型對象添加屬性
Cat.prototype.sayHello = function (){ // 給原型對象添加方法
   console.log('你們好,個人名字叫'+this.name);
}
var cat1 = new Cat(); // 實例對象
var obj = Cat.prototype; // 原型對象
console.log(obj);
console.log(cat1.constructor);
console.log(obj.constructor);
console.log(Cat.prototype === cat1.constructor.prototype);
 
屬性:constructor(構造器)
每一個對象都有一個隱藏屬性constructor,該屬性指向對象的構造函數(「類」)
經過上面的代碼咱們能夠看到,實例對象 cat1 和原型對象 obj 它們的構造器相同,都指向 Cat!
咱們換一種寫法:
function Cat(){}
Cat.prototype = { // 原型對象
   name: '小白',
   color: 'black',
   sayHello: function (){
       console.log('你們好,個人名字叫'+this.name);
   }
}
var cat1 = new Cat();
 
這種寫法更直觀看到原型對象是什麼,可是
console.log(Cat.prototype === cat1.constructor.prototype); 
console.log(Cat.prototype.constructor === Object); 
console.log(cat1.constructor === Object); 
 
此時 Cat.prototype 指向一個對象字面量方式定義的對象{},其構造器(constructor)指向的天然是根構造器 Object,因此 cat1 的構造器也指向根構造器 Object。
因而可知,屬性 constructor 並不可靠!
 
那麼,原型有什麼用呢?
原型的主要做用是用於「繼承」
var Person = function(name){
   this.name = name;
};
Person.prototype.type = 'human';
Person.prototype.getName = function(){
   console.log(this.name);
}
var p1 = new Person('jack');
var p2 = new Person('lucy');
p1.getName();
console.log(p1.type);
p2.getName();
console.log(p2.type); 
 
示例中經過給原型對象(Person.prototype)添加屬性方法
那麼由 Person 實例出來的普通對象(p1 p2)就繼承了這個屬性方法(type getName)
 
再看一個示例
Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
   console.log('嗨~你們好');
}
String.prototype.pin = function (){
   console.log(this+'&biubiu');
}
var str = 'yoyo';
var num = 666;
var arr = [];
var boo = true;
 
str.sayHi(); // 嗨~你們好
num.sayHi(); // 嗨~你們好
arr.sayHi(); // 嗨~你們好
boo.sayHi(); // 嗨~你們好
console.log(str.jdk); // abc123
console.log(num.jdk); // abc123
console.log(arr.jdk); // abc123
console.log(boo.jdk); // abc123
str.pin(); // yoyo&biubiu
num.pin(); // 報錯 num.pin is not a function
arr.pin(); // 報錯 arr.pin is not a function
boo.pin(); // 報錯 boo.pin is not a function
 
看出點什麼了嗎?
全部對象都繼承了Object.prototype原型上的屬性方法(換句話說它們都是Object的實例)
str 還繼承了String.prototype原型上的屬性方法
 
再看以前寫過的示例:
Date.prototype.getWeek = function () {
   var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
   var index = this.getDay();//0-6
   return arr[index];
}
var dates = new Date();
console.log(dates.getWeek()); // '星期一'
 
全部 Date 對象都將繼承 getWeek 方法
具體是怎麼實現的繼承,咱們就要講到原型鏈了
 
屬性:_ _ proto _ (原型)
每一個對象都有一個隱藏屬性_ _proto_ _,用於指向建立它的構造函數的原型
懵逼......怎麼又一個原型???
 
上面咱們講prototype是針對每一個函數對象,這個_ _ proto _ _是針對每一個對象
屬性_ _ proto _ _非官方標準屬性,但主流的瀏覽器基本都支持
 
var n = 123;
var s = 'jdk';
var b = true;
var a = [];
var f = function (){};
var o = {};
 
console.log(n.__proto__);
console.log(n.__proto__ === Number.prototype);
 
console.log(s.__proto__ === String.prototype);
console.log(a.__proto__ === Array.prototype);
console.log(f.__proto__ === Function.prototype);
console.log(o.__proto__ === Object.prototype);
console.log(b.__proto__ === Boolean.prototype);
 
對象 經過_ _ proto _ _指向原型對象,函數對象 經過prototype指向原型對象
那麼原型鏈呢,鏈在哪?
經過上面寫的示例,咱們來找找原型鏈:
Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
   console.log('嗨~你們好');
}
var str = 'yoyo';
str.sayHi();  // 嗨~你們好
console.log(str.jdk);  // 'abc123'
 
str 是怎麼訪問到 sayHi 方法和 jdk 屬性的呢?
瞭解一下方法 hasOwnProperty() ,用於判斷某個屬性是否爲該對象自己的一個成員
 
看看大體的訪問過程:
console.log(str.hasOwnProperty('sayHi'));//false str自身沒有sayHi方法
console.log(str.__proto__.hasOwnProperty('sayHi'));//false 原型對象也沒有sayHi方法
console.log(str.__proto__.__proto__.hasOwnProperty('sayHi'));//true 原型的原型有sayHi方法
str -> str._ _ proto _ _ -> str._ _ proto _ _ . _ _ proto _ _ 感受到什麼嗎?
咱們來描述一下執行過程:
str.sayHi() --> 自身查找 --> 沒有sayHi方法 --> 查找上層原型 str._ _ proto _ _ --> 指向 String.prototype對象 --> 沒有sayHi方法 --> 查找上層原型 String.prototype._ _ proto _ _ --> 指向Object.prototype對象 --> 找到sayHi方法 --> 執行sayHi方法
環環相扣,是否是像鏈條同樣呢?這個就是咱們所說的 原型鏈
下面的示例更形象:
原型鏈的最後是 null
若是還沒暈,恭喜你彷佛領悟到了某些人生的哲學:
《易經》-- ‘太極生兩儀,兩儀生四象,四象生八卦’
《道德經》-- ‘無,名天地之始’
是否是很熟悉,是否是很意外!
 
 
熟悉了原型和原型鏈,咱們來看看JS中常見實現「繼承」的方式:
// demo1 對象冒充繼承
function Cat(n,c){ // 貓 類
   this.name = n;
   this.color = c;
   this.trait = function (){
       console.log('賣萌~');
   }
}
Cat.prototype.skill = function (){ // 原型上的屬性方法
   console.log('抓老鼠');
}
 
// 需求:狗要賣萌,狗要多管閒事-抓老鼠
function Dog(n,c,f){ // 狗 類
   this.food = f;
   Cat.call(this,n,c); // 狗冒充貓,訪問貓的屬性方法
}
var dog1 = new Dog('二哈','yellow','shi');// 實例對象
console.log(dog1.name); // 二哈
dog1.trait(); // 賣萌
dog1.skill(); // 報錯 dog1.skill is not a function
咱們看到這種繼承方式有侷限性,「父類」原型上的屬性方法沒法繼承,因此二哈沒有抓老鼠的技能
 
 
// demo2 原型鏈繼承
function Cat(n,c){ // 貓 類
   this.name = n;
   this.color = c;
   this.trait = function (){
       console.log('賣萌~');
   }
}
Cat.prototype.skill = function (){// 原型上的屬性方法
   console.log('抓老鼠');
}
 
function Dog(n,c,f){ // 狗 類
   this.food = f;
}
Dog.prototype = new Cat(); // 把狗的原型指向貓的實例對象
 
var dog1 = new Dog('二哈','yellow','shi');
console.log(dog1.name); // undefined
console.log(dog1.food); // shi
dog1.trait(); // 賣萌~
dog1.skill(); // 抓老鼠
console.log(dog1.constructor); // Cat
問題一:
實例化對象的時候不能給「父類」傳參,致使訪問dog1.name沒有值
問題二:
有句臺詞:‘人是人媽生的,妖是妖媽生的 ’ 如今 dog1.constructor 指向 Cat,意味着 二哈 是貓媽生的!很顯然這不符合倫理,也不環保...
 
// demo3 混合繼承
function Cat(n,c){
   this.name = n;
   this.color = c;
   this.trait = function (){
       console.log('賣萌~');
   }
}
Cat.prototype.skill = function (){
   console.log('抓老鼠');
}
 
function Dog(n,c,f){
   this.food = f;
   Cat.call(this,n,c);// 對象冒充繼承
}
 
// Dog.prototype = new Cat(); 「構造器調用」獲得一個對象,容易產生一些反作用
 
Dog.prototype = Object.create(Cat.prototype);// 原型鏈繼承
// Object.create()用於建立一個空對象,並把該對象的[[Prototype]]連接到Cat.prototype
 
Dog.prototype.constructor=Dog;// 指正構造器
 
var dog1=new Dog('二哈','yellow','shi');
console.log(dog1.name);// 二哈
console.log(dog1.food);// shi
dog1.trait();// 賣萌~
dog1.skill();// 抓老鼠
console.log(dog1.constructor);// Dog
 
兩種方式結合能夠實現相對比較完美的「繼承」
別忘了指正構造器(類型),不能認賊做父!
 
小結:
在 JavaScript 中,沒有類,只有對象
多年來JS開發者們努力盡量地模擬面向類(山寨成某些看起來像「類」的東西)
原型機制和「類」不同,在面向類的語言中,能夠製造一個類的多個 拷貝(即「實例」),可是在 JavaScript 中,沒有這樣的拷貝處理髮生
 
原型機制是一個內部連接,其本質是行爲委託、對象間建立連接
這種連接在對一個對象進行屬性/方法引用,而這樣的屬性/方法不存在時實施
在這種狀況下,[[Prototype]] 連接告訴引擎在那個被連接的對象上查找這個屬性/方法
接下來,若是這個對象不能知足查詢,它的 [[Prototype]] 又會被查找,如此繼續。。。
這個在對象間的一系列連接構成了所謂的「原形鏈」
 
對象間的關係圖:
相關文章
相關標籤/搜索