JavaScript知識點總結(三)

JavaScript OOP, 對象, 對象建立, 繼承

//早期建立對象的方式
var jonslike = new Object();
jonslike.name = "jon";
jonslike.like = "wow";

jonslike.saylike = function(){
    console.log(this.name);
};

//對象字面量建立方式
var jonslike = {
    name : "jon",
    like : "wow",
    saylike : function(){
        console.log(jonslike.like); //亦可換成this, 由於指向當前對象
        console.log(this.like); //wow
    }
};

ES的兩種屬性, 數據屬性訪問器屬性javascript

數據屬性

數據屬性包含1個數據值的位置, 這個位置能夠讀取和寫入值java

名稱 描述
[[ Configurable ]] 表示可否經過delete刪除屬性而從新定義屬性, 可否修改屬性的特性, 或者可否把屬性修改成訪問器屬性, 默認值true
[[ Enumerable ]] 表示可否經過for-in循環返回屬性, 默認值true
[[ Writable ]] 表示可否修改屬性的值, 默認值true
[[ Value ]] 包含這個屬性的數據值. 讀取屬性值的時候, 從這個位置讀; 寫入屬性值的時候, 把新值保存在這個位置, 默認值undefined
var person = {
    name : Jon,  //value值變成Jon
};
修改數據屬性的默認特性 : 使用Object.defineProperty()方法.

接收3個參數, 屬性所在的對象, 屬性的名字, 一個描述符對象數組

描述符對象的屬性必須是 : configurable, enumerable, writable, value瀏覽器

//設置爲不可寫 :
var person = {};
person.name = "fire";
    Object.defineProperty(person, "name", {
        writable : false, //設置爲只讀, 不可寫
        value : "jon"
    });

person.name = "mark";
alert(person.name); //輸出仍是jon
//設置爲不可配置 :
var person = {};
Object.defineProperty(person, "name" , {
    configurable : false, //設置爲不可配置
    value : "jon",
});

person.name = "mark"; //無效!
delete person.name; //刪除無效!
alert(person.name); //依然能輸出jon

//NOTE : 配置configurable爲false時, 其餘3各特性也有相應的限制

訪問器屬性

訪問器屬性不包括數據值;安全

包含一對getter 與 setter 函數 (非必須)閉包

讀取訪問器屬性時, 會調用getter函數,該函數負責返回有效的值;app

寫入訪問器屬性時, 會調用setter函數,該函數負責決定如何處理數據;函數

名稱 描述
[[ Configurable ]] 表示可否經過delete刪除屬性而從新定義屬性, 可否修改屬性的特性, 或者可否把屬性修改成訪問器屬性, 默認值true
[[ Enumerable ]] 表示可否經過for-in循環返回屬性, 默認值true
[[ Get ]] 在讀取屬性時調用的函數, 默認值undefined
[[ Set ]] 在寫入屬性時調用的函數, 默認值undefined

定義訪問器屬性 : 使用Object.defineProperty()方法.測試

//建立book對象,
var book = {
  //定義兩個默認的屬性, _year和edition, 下劃線定義的屬性表示只能經過對象方法訪問
    _year : 2004,
    edition : 1
};

Object.defineProperty(book, "year", {
    get : function(){
        return this._year;
    },
    set : function(){
        if(newValue > 2004){
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    },
});

book.year = 2005;
alert(book.edition);  //2

定義多個屬性 : defineProperties()ui

能夠經過描述符一次過定義多個屬性

接收兩個參數 :

要添加和修改其屬性的對象

第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應.

var book = {};
Object.defineProperties(book, {
    _year : {
        value : 2004,
    },
    edition : {
        value : 1,
    },
    year : {
        get : funciton(){
            return this._year,
        },
        set : function(){
            if(newValue > 2004){
                this._year = newValue,
                this.edition += newValue - 2004,
                }
            }
        }
});

讀取屬性的特性 : 使用Object.getOwnPropertyDescriptor();

能夠取得給定屬性的描述符

接收兩個參數 :

屬性所在的對象, 要讀取其描述符的屬性名稱

返回值 : 一個對象, 若是返回的對象是訪問器屬性, 則這個對象的屬性有configurable, enumerable, get, set; 若是返回的對象是數據屬性, 則這個對象的屬性有configurable, enumerable, writable, value

var book = {};
Object.defineProperties(book, {
    _year : {
        value : 2004,
    },
    edition : {
        value : 1,
    },
    year : {
        get : funciton(){
            return this._year,
        },
        set : function(){
            if(newValue > 2004){
                this._year = newValue,
                this.edition += newValue - 2004,
                }
            }
        }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
//數據屬性
alert(descriptor.value);  //2004(最初的值)
alert(descriptor.configurable);  //false(最初的值)
alert(typeof descriptor.get);  // undefined

var descriptor = Object.getOwnPropertyDescriptor(book, year);
//訪問器屬性
alert(descriptor.value);  // undefined(訪問器沒有value屬性)
alert(descriptor.enumerable);  // false
alert(typeof descriptor.get);  // function(一個指向getter的指針)

建立對象

工廠模式

工廠模式抽象了建立具體對象的過程;

該模式沒有解決對象的識別問題(即怎樣知道一個對象的類型)

function createPerson(name, age, job){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayJob = function(){
      console.log(this.job);
  };
  return o;
}

var p1 = createPerson("Jon",25,"FrontEnd Developer");
var p2 = createPerson("Mark",24,"DBA");

p1.sayJob(); //FrontEnd Developer
p2.sayJob(); //DBA

構造函數模式

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayJob = function(){
      console.log(this.job);
  };
}

//使用new操做符建立Person的新實例
/*
調用構造函數會經歷如下步驟 : 
1. 建立一個新對象;
2.將構造函數的做用域賦給新對象(所以this就指向了這個新對象)
3.執行構造函數中的代碼(爲這個新對象添加屬性)
4.返回新對象
*/
var p1 = new Person("Jon", 25, "FrontEnd Developer");
var p2 = new Person("Mark", 24, "DBA");

p1.sayJob(); //FrontEnd Developer
p2.sayJob(); //DBA

//新對象具備一個constructor(構造函數)屬性, 指向原建立的構造函數(即Person)
console.log(p1.constructor == Person); //true
console.log(p2.constructor == Person); //true

//使用instanceof操做符檢測對象類型會更可靠
console.log(p1 instanceof Object); //Object是終極父類, 因此返回true
console.log(p1 instanceof Person); //p1是Person構造函數的實例
console.log(p2 instanceof Object); //Object是終極父類, 因此返回true
console.log(p2 instanceof Person); //p2是Person構造函數的實例

//構造函數自己也是函數, 因此能夠當作普通函數來調用(不使用new操做符調用)
Person("Martin", 27, "PHPer"); //添加到window對象(全局做用域中)
window.sayJob(); //PHPer

//在另外一個對象的做用域調用(使用call()或者apply())
var o1 = new Object();
Person.call(o1, "Kiki", 23, "Singer");
o1.sayJob(); //Singer

原型模式

每一個函數都有一個prototype(原型)屬性, 是一個指針, 指向一個對象

對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法;

prototype就是經過調用構造函數而建立的那個對象實例的對象

使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法;

即 : 

沒必要在構造函數中定義對象實例的信息, 而是能夠將這些信息直接添加到原型對象中;

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "f2e";
Person.prototype.sayName = function(){
  alert(this.name);
};

var p1 = new Person();
p1.sayName();  //jon

var p2 = new Person();
p2.sayName();  //jon

alert(p1.sayName == p2.sayName); //true
原型對象 :

不管什麼時候, 只要建立了一個新函數, 就會根據一組特定的規則爲該函數建立一個prototype屬性, 該屬性指向函數的 原型對象

即 : (新函數會建立一個prototype屬性指向原型對象)

默認狀況下, 全部原型對象會自動得到一個constructor構造函數屬性, 該屬性包含指向prototype屬性所在函數的指針

即 : (全部原型對象得到一個constructor(構造函數)屬性,包含指向prototype屬性所在函數的指針)

function Person(){};  //這是(空)構造函數,會有一個prototype屬性,指向(下面的)原型對象

//Person.prototype : 這是(構造函數的)原型對象, 會自動得到一個constructor(構造函數)屬性, 包含一個指向prototype屬性所在函數的指針,在這裏即上面的Person()函數; 即Person.prototype.constructor指向(上面的)Person()函數

//下面這些是(構造函數的)原型對象的自定義屬性s
Person.prototype.name = "Jon"; 
Person.prototype.age = 25;
Person.prototype.job = "f2e";
Person.prototype.sayName = function(){
  alert(this.name);
};

//這是實例,內部包含一個指針(內部屬性) [[Prototype]], 指向構造函數的原型對象(即上面的Person.prototype)
var p1 = new Person();
p1.sayName();  //jon

var p2 = new Person();
p2.sayName();  //jon

alert(p1.sayName == p2.sayName); //true

isPrototypeOf() : 肯定是否爲給定實例的原型

getPrototypeOf() [ES5] : 跟上面的功能同樣, 而且這方法能夠返回原型對象給定屬性的值

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

var p1 = new Person();
p1.sayJob(); //FrontEnd Developer

//測試Person是否爲p1的原型
console.log(Person.prototype.isPrototypeOf(p1)); //true

//若是支持ES5的getPrototypeOf()
if(Object.getPrototypeOf){
  //測試Person是否爲p1的原型
  console.log(Object.getPrototypeOf(p1) == Person.prototype); //true
  //輸出p1的name屬性的值
  console.log(Object.getPrototypeOf(p1).name); //Jon
}
注意實例中添加的屬性若是和原型的屬性同名, 則會覆蓋原型的屬性值
function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

var p1 = new Person();
p1.job = "DBA";
p1.sayJob(); //DBA

delete操做符能夠刪除實例的屬性

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

var p1 = new Person();
p1.job = "DBA";
p1.sayJob(); //返回自身添加的屬性, DBA

delete p1.job; //刪除p1的job屬性
p1.sayJob(); //返回原型的屬性, FrontEnd Developer

hasOwnProperty()能夠檢查一個屬性是位於實例仍是原型中, 屬於實例會返回true

in操做符會在對象能訪問給定屬性時返回true,不管是實例仍是原型 : (就是有這個屬性就會返回true)

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

var p1 = new Person();

console.log(p1.hasOwnProperty("name")); //實例中沒有本身定義的name屬性, 返回false
console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的)

p1.name = "Mark"; //本身定義一個實例中的name屬性, 覆蓋原型繼承而來的name
console.log(p1.hasOwnProperty("name")); //實例中有本身定義的name屬性(Mark), 返回true
console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的)

delete p1.name; //刪除p1實例的name屬性
console.log(p1.hasOwnProperty("name")); //p1的name屬性已經被delete操做符刪除, 因此如今又沒了自身實例的name屬性, 因此返回false
console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的)

能夠同時使用hasOwnProperty()in操做符, 以肯定給定的屬性是位於實例仍是原型中 :

in操做符只要能訪問給定屬性就返回true, hasOwnProperty()只在屬性屬於實例才返回true,

所以只要in操做符返回truehasOwnProperty()返回false, 就能肯定給定的屬性是原型的屬性

//obj表示要傳入的實例名稱, name表示要測試的實例屬性
function hasPrototypeProperty(obj, name){
  //若是傳入的實例屬性name不屬於該實例obj(取反), 而且(&&)實例obj中有該傳入的屬性name, 則返回
  return !obj.hasOwnProperty(name) && (name in obj);
}

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

var p1 = new Person(hasPrototypeProperty(p1, "name"));
console.log(hasPrototypeProperty(p1, "job")); //p1中尚未定義實例的job屬性, 只使用了原型繼承而來的job屬性, 因此返回true (hasOwnProperty()返回!false(取反false, 即true), in操做符返回true)

p1.job = "DBA"; //p1定義自身的實例屬性job
console.log(hasPrototypeProperty(p1, "job")); //false (!hasOwnProperty(job)爲 !true,即false, in返回true)

使用for-in返回能經過對象訪問的, 可枚舉的屬性(包括原型內和實例內的) :

var o = {
  name : "Jon",
  age : 25,
  saySth : function(){}
}

for(var prop in o){
  if(prop){
      console.log(prop);
  }
}

//name, age, saySth

Object.keys() [ES5]可得到全部可枚舉的屬性 :

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

//得到原型中全部可枚舉的屬性
var protoKeys = Object.keys(Person.prototype);
console.log(protoKeys); //"name", "age", "job", "sayJob"

var p1 = new Person;
p1.name = "Mark";
p1.nickname = "MM";
p1.age = 24;
p1.fakeAge = 21;
p1.job = "DBA";
p1.sayJob();

//若是經過實例調用, 則會獲得該實例中全部可枚舉的屬性
var keys = Object.keys(p1);
console.log(keys); //"name", "nickname", "age", "fakeAge", "job"

Object.getOwnPropertyNames()能夠獲得全部不管是否可枚舉的屬性

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

//得到原型中全部屬性(不管是否可枚舉)
var protoKeys = Object.getOwnPropertyNames(Person.prototype);
console.log(protoKeys); //"constructor", "name", "age", "job", "sayJob"

Object.keys()Object.getOwnPropertyNames()均可以替代for-in循環 (IE9+, ...)

使用對象字面量來建立新對象

function Person(){}

//這種方式其實已經重寫了默認的prototype對象, 此時constructor屬性已經再也不指向Person了, 而是指向了Object
Person.prototype = {
  name : "Jon",
  age : 25,
  job : "FrontEnd Developer",
  sayJob : function(){
  console.log(this.job);
  }
};

//因此此時雖然instanceof操做符還能返回正確的結果, 但constructor已經沒法肯定對象的類型了
var f1 = new Person();
console.log(f1 instanceof Person); //true
console.log(f1 instanceof Object); //true
console.log(f1.constructor == Person); //false
console.log(f1.constructor == Object); //true


//若是constructor的值很重要, 能夠像這樣把它設置回適當的值
//(修改上面的Person.prototype)
Person.prototype = {
  constructor : Person, //顯式的把constructor設置爲Person
  name : "Jon",
  age : 25,
  job : "FrontEnd Developer",
  sayJob : function(){
  console.log(this.job);
  }
};

//若是像上面同樣把constructor的值顯式的設置, 那麼它會變成可枚舉, 即[[Enumerable]]的值會變爲true,  若是要把它設置回不可枚舉, 可使用下面的ES5提供的新方法 : 
//重寫整個示例
function Person(){}

Person.prototype = {
  name : "Jon",
  age : 25,
  job : "FrontEnd Developer",
  sayJob : function(){
  console.log(this.job);
  }
};

//重設構造函數[ES5 only]
Object.defineProperty(Person.prototype, "constructor", {
  enumerable : false,
  value : Person
});

原型的動態性

原型中查找值的方法是一次搜索, 所謂動態性就是在原型對象上全部的修改都能當即從實例上反應出來, 即便是 先建立實例, 後修改原型 也是如此

function Person(){};

Person.prototype.name = "Jon";
Person.prototype.age = 25;
Person.prototype.job = "FrontEnd Developer";
Person.prototype.sayJob = function(){
  console.log(this.job);
};

//建立原型實例
var p1 = new Person();

//建立實例後再建立原型方法
Person.prototype.sayAge = function(){
  console.log(this.age);
}

//調用後建立的原型方法
p1.sayAge(); //照樣能工做! 輸出25
//但不能在建立原型實例後, 重寫整個原型對象
function Person(){}

//建立原型實例
var p1 = new Person();

//此時再定義Person的原型對象
Person.prototype = {
  constructor : Person,
  name : "Jon",
  job : "FrontEnd Developer",
  sayJob : function(){
      console.log(this.job);
  }
};

//記住, 實例的指針[[ prototype ]]僅指向原型, 而不指向構造函數
p1.sayJob(); //出錯! Uncaught TypeError: p1.sayJob is not a function

原生對象的原型

全部原生的引用類型(Object, Array, String, etc...), 都是使用這種原型模式建立的, 都在其構造函數上定義了方法

經過原生對象的原型, 不只能夠取得全部默認方法的引用, 並且也能夠定義新方法. 能夠像修改自定義對象的原型同樣修改原生對象的原型: 便可以隨時添加方法(但不推薦) :

console.log(typeof Array.prototype.sort); //function
console.log(typeof String.prototype.substr); //function

//爲原生引用類型String添加方法(不推薦) : 
String.prototype.startsWith = function(text){
  return this.indexOf(text) == 0;
}

var s1 = "Hi Jon";
console.log(s1.startsWith("Hi")); //true

原型模式的問題 :

省略了爲構造函數初始化參數的環節, 致使全部新建的實例都會取得相同的默認值

最大的問題是其共享的本性所致使的, 對於引用類型值的屬性來講問題很是突出 :

function Person(){}

Person.prototype = {
  constructor : Person,
  name : "Jon",
  job : "FrontEnd Developer",
  friends : ["Lucy","Jeniffer"],
  sayJob : function(){
      console.log(this.job);
  }
};

var p1 = new Person();
var p2 = new Person();

p1.friends.push("Quinene");

console.log(p1.friends); //"Lucy", "Jeniffer", "Quinene"
console.log(p2.friends); //"Lucy", "Jeniffer", "Quinene"
console.log(p1.friends === p2.friends); //true

組合使用構造函數模式和原型模式(最經常使用)

構造函數模式用於定義實例屬性, 原型模式用於定義方法和共享的屬性 :

結果每一個實例都有本身的一份實例屬性的副本, 但同時又共享着對方法的引用,最大限度的節省了內存:

這種模式還支持向構造函數傳參 :

function Person(name, age, job){
  //定義實例屬性(未來建立實例時不會相同的屬性s)
  this.name = name;
  this.age = age;
  this. job = job;
  this. friends = ["Mark", "Martin"];
}

Person.prototype = {
  //構造函數屬性指回Person
  cosntructor : Person,
  //定義方法
  sayJob : function(){
    console.log(this.job);
  },
  //定義共享屬性
  country : "China"
};

//建立實例
var p1 = new Person("Jon", 25, "FrontEnd Developer");
var p2 = new Person("Percy", 26, "DBA");

//爲實例p1的friends屬性添加值
p1.friends.push("Jeniffer");

console.log(p1.friends); //"Mark", "Martin", "Jeniffer"
console.log(p2.friends); //"Mark", "Martin"
console.log(p1.friends == p2.friends); //false
console.log(p1.sayJob == p2.sayJob); //true
console.log(p1.country == p2.country); //true

動態原型模式

動態原型模式把全部信息都封裝在構造函數中, 而經過在構造函數中初始化原型(僅在必要的狀況下), 又保持了同時使用構造函數和原型的優勢 : 

即 能夠經過檢查某個應該存在的方法是否有效, 來決定是否須要初始化原型

function Person(name, age, job){

  //屬性
  this.name = name;
  this.age = age;
  this.job = job;
  
  //方法
  if(typeof this.sayJob != "function"){
      Person.prototype.sayJob = function(){
        console.log(this.job);
    }
  }
}

var p1 = new Person("Jon", 25, "F2E");
p1.sayJob(); //F2E
  • 這裏只在sayJob()方法不存在的狀況下, 纔會將它添加到原型中.

  • 這段代碼只會在初次調用構造函數時纔會執行.

  • 這裏對原型所作的修改, 也會當即在全部實例中獲得反映.

  • if語句檢查的能夠是初始化以後應該存在的任何屬性和方法—— 沒必要用一大堆if語句判斷每一個屬性的方法,只要其中檢查一個便可;

  • 這種模式建立的對象能夠用instanceof操做符肯定它的類型

寄生構造函數模式

基本思路是建立一個函數, 這個函數做用僅僅是封裝建立對象的代碼, 而後再返回新建立的對象.

function Person(name, age, job){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayJob = function(){
    console.log(this.name);
  };
  return o;
}

var p1 = new Person("Jon", 25, "F2E");
p1.sayJob(); //F2E

Person函數建立了一個新對象o, 並以相應的屬性和方法初始化該對象, 而後把它返回.

除了使用new操做符並把使用的包裝函數叫作構造函數外, 這個模式跟工廠模式實際上是同樣的.

構造函數在不返回值的狀況下, 默認會返回新對象的實例, 而經過在構造函數的末尾添加一個return語句, 能夠重寫調用構造函數時返回的值.

這種模式在特殊的狀況下用來爲對象建立構造函數.假設咱們想建立一個具備額外方法的特殊數組,

由於不能直接修改Array構造函數, 所以可使用這種模式 :

function SpecialArray(){
  //建立一個數組用於接收傳入的值
  var values = new Array();
  //而後使用push方法(用構造函數接收到的全部參數)初始化了數組的值;
  values.push.apply(values, arguments);
  //給數組實例添加了一個toPipedString()方法, 該方法返回以短橫線分割的數組值;
  values.toPipedString = function(){
      return this.join("-");
  };
  //將數組以函數值的形式返回.
  return values;
}

var colorsArr = new SpecialArray("red", "blue", "purple");
console.log(colorsArr.toPipedString()); //red-blue-purple
//關於該模式 : 首先, 返回的對象與構造函數或者構造函數的原型屬性之間沒有關係;也就是說, 構造函數返回的對象與在構造函數外部建立的對象沒有什麼不一樣.爲此不能依賴instance操做符來肯定對象類型.
console.log(colorsArr instanceof SpecialArray); //false

穩妥構造函數模式

穩妥對象 : 沒有公共屬性, 其方法也不引用this的對象.

適合在安全的環境中(禁止使用this和new), 或者在防止數據被其餘應用程序改動時使用

穩妥構造函數遵循與寄生構造函數相似的模式, 但有兩點不一樣 : 

一是新建立對象的實力方法不引用this,

二是不適用new操做符調用構造函數 :

function Person(name, age, job){
  //建立要返回的對象
  var o = new Object();
  
  //能夠在這裏定義私有變量和方法
  
  //添加方法
  o.sayJob = function(){
      console.log(job);
  }
  
  //返回對象
  return o;
}

/*這種方式建立的對象中, 除了使用sayJob()方法外, 沒有其餘辦法訪問job的值*/

//使用穩妥的Person構造函數
var p1 = new Person("Jon", 25, "FrontEnd Developer");
p1.sayJob(); //FrontEnd Developer
console.log(p1.job); //嘗試直接訪問job屬性會返回undefined

繼承

許多OO語言都支持兩種繼承方式 : 

接口繼承 : 只繼承方法簽名

實現繼承 : 繼承實際的方法

因爲函數沒有簽名, 在ES中沒法實現接口繼承.ES只支持實現繼承, 並且其 實現繼承 主要是依靠原型鏈實現的.

方法簽名由方法名稱和一個參數列表(方法的參數的順序和類型)組成。

方法簽名應該以下所示,相應的可變參數分別使用String和Exception聲明:

Log.log(String message, Exception e, Object... objects) {...}


原型鏈

利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法.

簡單回顧下構造函數, 原型, 實例的關係 : 

每一個構造函數都有一個原型對象( prototype ), 原型對象都包含一個指向構造函數的指針( constructor ), 而每一個實例都包含一個指向原型對象的內部指針( [[ prototype ]], __proto__ )

那麼,假如咱們讓原型對象(prototype)等於另外一個類型的實例, 那麼此時的原型對象將包含一個指向另外一個原型的指針.

相應地, 另外一個原型中也包含着一個指向另外一個構造函數的指針.

假如另外一個原型又是另外一個原型的實例, 那麼上述關係依然成立, 如此層層遞進, 就構成了實力與原型的鏈條, 這就是所謂的原型鏈的概念.

實現原型鏈的基本模式 :

/*定義兩個類型, SuperType和SubType*/
function SuperType(){
  //SuperType本身的屬性
  this.property = true;
}

SuperType.prototype.getSuperValue = function(){
  //SuperType本身的方法
  return this.property;
}

function SubType(){
  //SubType本身的屬性
  this.subproperty = false;
}

/*SupType經過建立SuperType()的實例繼承了SuperType, 並賦值給SubType.prototype, 即SubType的原型對象實現的本質是重寫原型對象, 代之以一個新類型的實例.
換句話說, 原來存在於SuperType的實例中的全部屬性和方法, 如今也存在於SubType.prototype中了
*/
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
  //添加SubType本身的方法, 這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法
  return this.subproperty;
}

//建立一個新實例
var instance = new SubType();
console.log(instance.getSuperValue()); //true

//測試是否爲Object, SuperType, SubType的實例
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true

console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true

關係如圖所示 : 

最終結果 :

instance實例指向SubType的原型, SubType的原型又指向SuperType的原型.

getSuperValue()方法仍然還在SuperType.prototype中, 但property則位於SubType.prototype中.

這是由於, property是一個實例屬性,而getSuperType()則是一個原型方法

既然SubType.prototype如今是SuperType的實例, 那麼prototype固然就位於該實例中了.

要注意,實例的 instance.constructor如今指向的是SuperType, 這是由於SubType的原型如今指向了另外一個對象—— SuperType的原型.

而這個原型對象的constructor屬性指向的是SuperType.

別忘記了默認的原型 :

全部引用類型默認都繼承了Object, 而這個繼承也是經過原型鏈實現的.

要記住, 全部函數的默認原型都是Object的實例, 所以默認原型都會包含一個內部指針指向Object.prototype.

這也正是全部自定義類型都會繼承toString(), valueOf()的根本緣由.

因此, 上面例子展現的原型鏈應該還包含另外一個繼承層次 : (完整的原型鏈以下)

肯定原型和實例的關係 :
  • 使用instanceof 操做符, 測試實例和原型鏈中出現過的構造函數, 若是存在就會返回true

  • 使用isPrototypeOf() 方法, 只要是原型鏈中出現過的原型, 均可以說是該原型鏈所派生的實例的原型,所以該方法會返回true

//上面第一段代碼的最後片斷 : 
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true

console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
謹慎的定義方法( 子類型重寫超類型已存在的方法 or 子類型添加超類型自己不具備的方法 ) :

給原型添加方法的代碼必定要放在替換原型的語句以後 :

function SuperType(){
  this.property = true;
}

SuperType.prototype.getSuperValue = function(){
  return this.property;
}

function SubType(){
  this.subproperty = false;
}

//從SuperType繼承
SubType.prototype = new SuperType();

//SubType本身的新方法
SubType.prototype.getSubValue = function(){
  return this.subproperty;
}

//SubType繼承的父類方法getSuperValue()被重寫, 但只會重寫SubType自身的getSuperValue(), 不會影響上一級父類原來的方法, 即若是調用的是SuperType的getSuperValue()方法的話仍是會返回原來的true.
SubType.prototype.getSuperValue = function(){
  return false;
}

//建立實例
var ins1 = new SubType();
var ins2 = new SuperType();

console.log(ins1.getSuperValue()); //false, 重寫的方法
console.log(ins2.getSuperValue()); //true, SubType重寫getSuperValue()方法並不會影響父類原有的方法
經過原型鏈實現繼承時, 不能使用對象字面量建立原型方法, 由於這樣會重寫原型鏈 :
function SuperType(){
  this.property = true;
}

SuperType.prototype.getSuperValue = function(){
  return this.property;
}

function SubType(){
  this.subproperty = false;
}

//從SuperType繼承
SubType.prototype = new SuperType();

/*剛剛把SuperType的實例賦值給SubType的原型⬆️, 又使用對象字面量⬇️把原型替換 SubType.prototype = {...}, 因此如今SubType的原型包含的是 一個屬於Object的實例而不是SuperType的, 原先的原型鏈已經被切斷, SubType與SuperType已經沒有任何關係了*/

//使用對象字面量把 原型替換
SubType.prototype = {
  getSubValue : function(){
      return this.subproperty;
  },
  someOtherMethod : function(){
      return false;
  }
}

var ins1 = new SubType();
console.log(ins1.getSuperValue()); //Uncaught TypeError: ins1.getSuperValue is not a function
原型鏈的問題 :

最主要的問題來自包含引用類型值的原型.

以前說過, 包含引用類型值的原型屬性會被全部實例共享.

這也是爲何要在構造函數中, 而不是原型對象中定義屬性的緣由.

第二個問題是, 在建立子類型的實例時, 不能向超類型的構造函數傳遞參數

在經過原型來實現繼承時, 原型實際上會變成另外一個類型是實例( SubType.prototype = new SuperType(); ), 因而, 原先的實例屬性也就瓜熟蒂落的變成了如今的原型屬性了.

function SuperType(){
  this.colors = ["red", "green", "blue"];
}

function SubType(){}

SubType.prototype = new SuperType();

var ins1 = new SubType();
console.log(ins1.colors); // "red", "green", "blue"
//在ins1添加colors屬性的屬性值
ins1.colors.push("purple");
console.log(ins1.colors); //"red", "green", "blue", "purple"

var ins2 = new SubType();
//ins1中添加到colors中的屬性值直接被添加到了SubType()的原型屬性裏面, 致使後來新增的實例也繼承了這些屬性
console.log(ins2.colors); //"red", "green", "blue", "purple"]
基於以上問題, 單獨使用原型鏈, 在開發中是不多的.

借用構造函數( 僞造對象, 經典繼承 )

思路 : 在子類型構造函數的內部調用超類型的構造函數.

函數只不過是在特定環境中執行代碼的對象, 所以能夠經過apply()call()方法也能夠在(未來)新建立的對象上執行構造函數

function SuperType(){
  this.colors = ["green", "blue", "purple"];
}

function SubType(){
  //繼承自SuperType
  //當SubType(){...}被實例化後, SuperType()函數中定義的全部對象初始化代碼就會被執行
  SuperType.call(this);
}

var ins1 = new SubType();
ins1.colors.push("red");
console.log(ins1.colors); //"green", "blue", "purple", "red"

var ins2 = new SubType();
console.log(ins2.colors); //"green", "blue", "purple"
//相比原型鏈, 借用構造函數還有一個很大的優點, 就是子類型的構造函數能夠向超類型的構造函數傳遞參數

function SuperType(name){
  //父類構造函數接受一個name函數, 並賦值給一個屬性
  this.name = name;
}

function SubType(){
  /*在SubType()構造函數中調用SuperType()構造函數時,
  其實是爲SubType的實例設置了name屬性*/
  SuperType.call(this, "Jon");
  
/*爲了確保SuperType構造函數不會重寫子類型的屬性, 能夠在調用父類構造函數後,再添加應該在子類型中定義的屬性*/
  this.age = 25;
}

var ins1 = new SubType();
console.log(ins1.name); //Jon
console.log(ins1.age); //25

若是僅僅是借用構造函數, 那麼也沒法避免構造函數模式存在的問題—— 方法都在構造函數內部定義, 那麼函數複用就無從談起了.

並且在超類型的原型中定義的方法, 對子類型而言也是不可見的, 結果全部類型都只能使用構造函數模式.

因此這種方式也是不多單獨使用的

組合繼承( 僞經典繼承 )

指的是將 原型鏈借用構造函數 的技術組合到一塊, 從而發揮兩者之長的一種繼承模式.

思路是, 使用 原型鏈 實現 對原型屬性和方法的繼承 , 而經過 借用構造函數 來實現對 實例屬性的繼承

這樣, 既經過在原型上定義方法實現了函數複用, 又可以保證每一個實例都有本身的屬性. 因此這成爲JavaScript中經常使用的繼承方式

function SuperType(name){
  //父類定義兩個屬性name和colors
  this.name = name;
  this.colors = ["blue", "red", "yellow"];
}

//父類定義原型方法sayName
SuperType.prototype.sayName = function(){
  console.log(this.name);
}

function SubType(name, age){
  //SubType構造函數在調用SuperType構造函數時傳入了name參數
  SuperType.call(this, name);
  //而後定義本身的屬性age
  this.age = age;
}

//將SuperType的實例賦值給SubType的原型
SubType.prototype = new SuperType(); //name, colors[], sayName()
SubType.prototype.constructor = SubType; //構造函數指回本身
//在該新原型上定義了方法sayAge()
SubType.prototype.sayAge = function(){
  console.log(this.age);
}

//兩個不一樣的SubType實例既分別擁有本身的屬性————包括colors屬性, 又可使用相同的方法了
var ins1 = new SubType("Jon", 25);
ins1.colors.push("purple");
console.log(ins1.colors); //"blue", "red", "yellow", "purple"
ins1.sayName(); //Jon
ins1.sayAge(); //25

var ins2 = new SubType("Mark", 24);
console.log(ins2.colors); //"blue", "red", "yellow"
ins2.sayName(); //Mark
ins2.sayAge(); //24

原型式繼承

藉助原型能夠基於已有的對象建立新對象, 同時還沒必要所以建立自定義類型.

/*
在object()函數內部, 先建立了一個臨時性的構造函數F(){},
而後將傳入的對象o做爲這個構造函數F(){}的原型,
最後返回了這個臨時類型的新實例.
從本質上講, object()對傳入其中的對象o執行了一次淺複製
*/
/*
這種繼承方式要求你必須有一個對象能夠做爲另外一個對象的基礎,
把它傳給object()函數,而後再根據具體需求對獲得的對象加以修改便可.
*/
function object(o){
  function F(){}
  F.prototype = o;
  return new F;
}
/*
這個例子中, 能夠做爲另外一個對象的基礎是person對象
*/
var person = {
  name : "Jon",
  colorsLike : ["black", "white"]
}
/*
把它(person對象)傳入到object()函數中, 而後該函數就會返回一個新對象( anotherPerson1 和 anotherPerson2 ), 
這兩個新對象把person做爲原型, 全部它們的原型中就包含一個基本類型值屬性和
一個引用類型值屬性,這意味着person.colorsLike不只於person全部,同時也會被
anotherPerson1, anotherPerson2共享, 實際上, 就至關於又建立了person對象的兩個副本
*/
var anotherPerson1 = object(person); //anotherPerson1如今有了person的全部屬性(這裏是name和colorsLike[])
console.log(anotherPerson1.name); //person原有的name屬性值, 輸出Jon
console.log(anotherPerson1.colorsLike); //person原有的colorsLike[]數組, 輸出["black", "white"]
anotherPerson1.name = "Percy"; //修改anotherPerson1的name屬性爲本身的值
anotherPerson1.colorsLike.push("purple"); //添加anotherPerson1本身喜歡的顏色
console.log(anotherPerson1.name); //Percy
console.log(anotherPerson1.colorsLike); //["black", "white", "purple"]
console.log(person.name); //Jon
console.log(person.colorsLike); //person的colorsLike數組值已經被anotherPerson1添加的屬性影響, 此時也輸出了["black", "white", "purple"]

var anotherPerson2 = object(person);
console.log(anotherPerson2.name); //Jon
console.log(anotherPerson2.colorsLike); //["black", "white", "purple"]
anotherPerson2.colorsLike.push("red"); //再push一個
console.log(anotherPerson2.colorsLike); //["black", "white", "purple", "red"]
console.log(person.colorsLike); //再度被anotherPerson2新增的值影響, 輸出["black", "white", "purple", "red"]

ES5新增了一個方法Object.create()規範了原型式繼承, 該方法接收兩個參數, 一個用做新對象的原型的對象和一個(可選)一個爲新對象定義額外屬性的對象

瀏覽器支持, IE9+和各現代瀏覽器

仍是直接看例子比較直觀 :

傳入一個參數的時候, 這個方法跟上面object()方法的行爲相同 :

var person = {
  name : "Jon",
  colorsLike : ["black", "white"]
};

console.log(person.colorsLike); // ["black", "white"]

//傳入一個參數的時候, 這個方法跟上面object()方法的行爲相同
var anotherPerson = Object.create(person);
anotherPerson.name = "Percy";
anotherPerson.colorsLike.push("purple");

console.log(anotherPerson.name); //Percy
console.log(anotherPerson.colorsLike); //["black", "white", "purple"]
console.log(person.name); //Jon
console.log(person.colorsLike); //["black", "white", "purple"]

傳入兩個參數的時候, 第二個參數與Object.defineProperties()方法的第二個參數格式相同 : 每一個屬性都是經過本身的描述符定義的, 以這種方式指定任何屬性都會覆蓋原型對象上的同名屬性

var person = {
  name : "Jon",
  colorsLike : ["black", "white"]
};

console.log(person.colorsLike); // ["black", "white"]

//傳入兩個參數
var anotherPerson = Object.create(person, {
  name : {
    value : "Martin"
  }
});

console.log(anotherPerson.name); //Martin

寄生式繼承

與原型式繼承緊密相關的思路, 與寄生構造函數和工廠模式相似, 即建立一個僅用於封裝繼承過程的函數, 該函數在內部以某種形式來加強對象, 最後再像真的是它作了全部工做同樣返回對象

function object(o){
  function F(){}
  F.prototype = o;
  return new F;
}

function createAnother(original){
  var clone = object(original); //經過調用函數建立一個新對象
  clone.sayHi = function(){ //以某種方式加強這個對象(添加自身方法或者屬性等)
      console.log("Good Day!");
  };
  return clone; //返回該對象
}

var person = {
  name : "Jon",
  friends : ["Martin", "Jeniffer"]
};

/*
這個實例中的代碼 基於person 返回了一個新對象————anotherPerson
該對象不只具備person全部屬性和方法, 並且還有本身的sayHi()方法
*/
/*
在主要考慮對象而不是自定義類型和構造函數的狀況下, 寄生式繼承也是一種有用的方式, 前面示範繼承模式使用的object()函數並非必須的, 任何可以返回新對象的函數都適用於此模式
*/

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //Good Day!

寄生組合式繼承

前面說過, 組合繼承是JS中最經常使用的繼承模式, 不過它也有本身的不足

組合繼承 最大的問題是, 不管在什麼狀況下, 都會調用兩次父類型構造函數, 一次是在建立子類型原型的時候, 一次是在子類型構造函數內部 :

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"]
}

SuperType.prototype.sayName = function(){
  console.log(this.name);
}

function SubType(name, age){
  SuperType.call(this, name);  //第二次調用SuperType()
  this.age = age;
}

/*
第一次 調用SuperType構造函數時, SubType.prototype會獲得兩個屬性,
name和colors[], 它們都是SuperType的實例屬性, 只不過如今位於SubType的原型中;
當調用SubType構造函數時, 又會再一次調用一次SuperType構造函數, 這一次又在新對象上建立了實例屬性name和colors[], 因而, 這兩個屬性就遮蔽了原型中的兩個同名屬性
*/
SubType.prototype = new SuperType(); //第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
  console.log(this.age);
}

以下圖 :

寄生組合式繼承, 即經過借用構造函數來繼承屬性, 經過原型鏈的混成模式來繼承方法.

思路是, 沒必要爲了指定子類型的原型而調用構造超類型的構造函數, 咱們所須要的無非就是超類型的一個副本而已 ⬇️

本質上, 就是使用寄生式繼承來繼承超類型的原型, 而後再將結果指定給子類型的原型.

基本模式以下所示. ⬇️

function object(o){
function F(){}
F.prototype = o;
return new F();
}

/*
寄生組合式繼承的最簡單形式, 這個函數接收兩個參數, 子類型構造函數和超類型構造函數;
在函數內部,第一步是建立超類型原型的一個副本, 第二步是爲建立的的副本添加constructor屬性, 從而彌補因重寫而失去默認的constructor屬性;
最後一步, 將新建立的對象(即副本)賦值給子類型的原型,這樣咱們就能夠調用inheritPrototype()函數的語句,去替換前面例子中未知類型原型賦值的語句了(41行)
*/
function inheritPrototype(subType, superType){
  var prototype = object(superType.prototype);   //建立對象
  prototype.constructor = subType;               //加強對象
  subType.prototype = prototype;                 //指定對象
}

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
  console.log(this.name);
};

function SubType(name, age){  
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType); //調用inheritPrototype()函數

SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new SubType("Jon", 25);
instance1.colors.push("black");
console.log(instance1.colors);  //"red,blue,green,black"
instance1.sayName();      //"Jon"
instance1.sayAge();       //25


var instance2 = new SubType("Mark", 24);
console.log(instance2.colors);  //"red,blue,green"
instance2.sayName();      //"Mark"
instance2.sayAge();       //24

函數表達式

經常使用JS定義函數方式有兩種 :

第一種是函數聲明 :

function Person(name){
  this.name = name;
  console.log("name is " + this.name);
}
Person("Jon"); //name is Jon


//函數聲明支持函數聲明提高, 即執行代碼前會先讀取函數聲明, 那麼函數聲明能夠放在調用它的代碼以後而不出錯 : 
Person("Jon"); //works ! 輸出name is Jon
function Person(name){
  this.name = name;
  console.log("name is " + this.name);
}

第二種是函數表達式 :

var Person = function(name){
  this.name = name;
  console.log(this.name);
}

Person("Jon"); //Jon

//函數表達式不支持函數聲明提高
Person("Jon"); //Uncaught TypeError: Person is not a function
var Person = function(name){
  this.name = name;
  console.log(this.name);
}

要在使用條件語句後面執行函數的話, 條件語句內的函數必須使用函數表達式的方式定義, 若是使用函數聲明方式定義, 會在不一樣的瀏覽器致使不一樣問題的發生 :

//條件語句內的函數定義必須使用函數表達式
var b = true;
if(b){
  sayColors = function(){
      console.log(this.color);
  };
}else{
  console.log("error!");
}
匿名函數
function createComparisonFunction(propertyName){
  //這裏返回的就是匿名函數, 它能賦值給一個變量, 或者以其餘的方式調用
  return function(object1, object2){
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    }else if(value1 > value2){
      return 1;
    }else{
      return 0;
    }
  };
}

遞歸

遞歸函數是一個函數經過名字調用自身的狀況下構成的

//遞歸階乘函數
function factorial(num){
  if(num <= 1){
      return 1;
  }else{
      return num * factorial(num - 1); 
  }
}

//注意以下調用會產生錯誤
var anotherFactorial = factorial; //把factorial()函數保存在一個變量中
factorial = null; //把factorial函數設置爲null
console.log(anotherFactorial(3)); //Uncaught TypeError: factorial is not a function
//使用arguments.callee解決上面的問題
//arguments.callee是一個指向當前正在執行的函數的指針, 所以能夠用它來實現對函數的遞歸調用
//嚴格模式下不容許使用arguments.callee
function factorial(num){
  if (num <= 1) {
    return -1;
  }else{
    //arguments.callee代替了函數名factorial
    return num * arguments.callee(num - 1);
  }
}
//解決嚴格模式下不容許使用arguments.callee的問題
//使用命名函數表達式來達成相同的結果
var factorial = (function f(num){ //建立一個名爲f()的命名函數表達式, 賦值給factorial
  if(num <= 1){
      return 1;
  }else{
      return num * f(num - 1);
  }
});

閉包

注意匿名函數閉包不要混淆.

閉包指的是有權訪問 另外一個函數做用域中的變量函數

建立閉包經常使用的方式, 就是在一個函數內部建立另外一個函數 :

function createComparisonFunction(propertyName){
  return function(object1, object2){
    //value1和value2訪問了外部函數的變量propertyName, 即便該內部函數被返回或被其餘地方調用, 也不影響它訪問外部函數的propertyName變量(由於該外部變量在本內部函數的做用域內)
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    }else if(value1 > value2){
      return 1;
    }else{
      return 0;
    }
  };
}

理解 :

//定義compare函數
function compare(value1, value2){
  if(value1 < value2){
     return -1;   
  }else if(value1 > value2){
      return 1;
  }else{
      return 0;
  }
}

//在全局做用域中調用函數, 從做用域鏈的優先級來分的話, 外部函數的活動對象始終處於第二位, 外部函數的外部函數的活動對象處於第三位 ...(以此類推), 直到做爲做用域鏈終點的全局執行環境
var result = compare(5, 8);

//在調用compare()函數時, 會建立一個包含arguments, value1, value2的活動對象(在做用域鏈的優先級處於第一位), 全局執行環境的變量對象(包含result和compare)在compare()執行環境的做用域鏈優先級處於第二位

做用域鏈優先級圖示 :

後臺的每個執行環境都有一個表示變量的對象 — — 變量對象

全局環境的變量對象始終存在, 而像compare()函數這樣的局部環境的變量對象, 則只在函數執行的過程當中存在.

  • 建立compare()函數時, 會建立一個預先包含全局變量對象的做用域鏈, 該做用域鏈會被保存在內部的[[ Scope ]]屬性中

  • 調用compare()函數時, 會爲函數建立一個執行環境

  • 而後經過複製函數的[[ Scope ]]屬性中的對象構建起執行環境的做用域鏈

未完待續...

模仿塊級做用域

TODO

私有變量

TODO

相關文章
相關標籤/搜索