理解Javascript的動態語言特性

龍恩0707發表於2015-06-06

理解Javascript的動態語言特性

Javascript是一種解釋性語言,而並非編譯性,它不能編譯成二進位制檔案。

理解動態執行與閉包的概念

動態執行:javascript提供eval()函式,用於動態解釋一段文字,並在當前上下文環境中執行。

首先我們需要理解的是eval()方法它有全域性閉包和當前函式的閉包,比如如下程式碼,大家認為會輸出什麼呢?

var i = 100;
function myFunc() {
    var i = 'test';
    eval('i = "hello."');
}
myFunc();
alert(i); // 100

首先我們來看看先定義一個變數i=100,然後呼叫myFunc這個函式,然後修改區域性變數i,使他從值 ’test’變成’hello’, 但是我們知道eval的含義是立即執行一段文字的含義;因此上面的程式碼我們可以寫成如下程式碼:

var i = 100;
function myFunc() {
    var i = 'test';
    (function(){
        return (i = "hello.");
    })();
}
myFunc();
alert(i); // 100

這樣就很明顯了,執行myFunc()這個方法後,i的值從test變為hello的值,但是由於是閉包,i的值為hello,它不能被外部使用,所以瀏覽器列印的都是100值;

我們都知道eval()是javascript的全域性物件Global提供的方法,而如果要訪問Global物件的方法,可以通過宿主物件-在瀏覽器中是window來提供;按道理來說,下面的程式碼應該也是輸出100;如下:

var i = 100;
function myFunc() {
    var i = 'test';
    window.eval('i="hello."');
}
myFunc();
alert(i);

然後不幸的是:在IE下不管是window.eval()還是eval()方法輸出的都是100;但是在標準瀏覽器下使用window.eval(),輸出的是hello,使用eval()方法的輸出的是100; 因為IE下使用的是JScript引擎的,而標準瀏覽器下是SpiderMonkey Javascript引擎的,正是因為不同的javascript引擎對eval()所使用的閉包環境的理解並不相同。

理解eval使用全域性閉包的場合

如下程式碼:

var i = 100;
function myFunc() {
    var i = 'test';
    window.eval('i="hello."');
}
myFunc();
alert(i);

在標準瀏覽器下,列印的是hello,但是在IE下列印的是100;如果使用如下程式碼:

var i = 100;
function myFunc() {
    var i = 'test';
    //window.eval('i="hello."');
    eval.call(window,'i="hello"');
}
myFunc();
alert(i);

也是一樣的,也是給eval方法提供一種訪問全域性閉包的能力;但是在IE下Jscript的eval()沒有這種能力,IE下一隻列印的是100;不過在IE下可以使用另一種方法得到一個完美的結果,window.execScript()方法中執行的程式碼總是會在全域性閉包中執行,如下程式碼:

var i = 100;
function myFunc() {
    var i = 'test';
    window.execScript('i="hello."');
    //eval.call(window,'i="hello"');
}
myFunc();
alert(i); // 列印hello

JScript()引擎使用execScript()來將eval在全域性閉包與函式閉包的不同表現分離出來,而Mozilla的javascript引擎則使用eval()函式的不同呼叫形式來區分它們。二者實現方法有不同,但是可以使用不同的方式實現全域性閉包;

理解eval()使用當前函式的閉包

一般情況下,eval()總是使用當前函式的閉包,如下程式碼:

var i = 100;
function myFunc() {
    var i = 'test';
    eval('i="hello."');
}
myFunc();
alert(i);  // 100

如上程式碼:因為eval作用與是函式內的程式碼,所以輸出的是全域性變數i等於100;

eval()總是被執行的程式碼文字視為一個程式碼塊,程式碼塊中包含的是語句,複合語句或語句組。

我們可以使用如下程式碼取得字串,數字和布林值;

eval('true'); // true

eval('"this is a char"');  // string

eval('3');  // 數字3

但是我們不能使用同樣的方法取得一個物件;如下程式碼:

eval('{name:"MyName",value:1}');

如上程式碼會報錯;如下:Uncaught SyntaxError: Unexpected

其實如上那樣寫程式碼,{name:"MyName",value:1},eval會將一對大括號視為一個複合語句來標識,如下分析:

  1. 第一個冒號成了 “標籤宣告”標示符。
  2. {“標籤宣告”的左運算元}name成了標籤。
  3. MyName成了字串直接量;
  4. Value成了變數標示符。
  5. 對第二個冒號不能合理地作語法分析,出現語法分析期異常;

如果我們只有這樣一個就不會報錯了,如下程式碼:

eval('{name:"MyName"}')

輸出"MyName";

那如果我們想要解決上面的問題要如何解決呢?我們可以加一個小括號包圍起來,使其成為一個表示式語句,如下程式碼:

eval('({name:"MyName",value:1})')

輸出一個物件Object {name: "MyName", value: 1}

但是如下的匿名函式加小括號括起來在IE下的就不行了,如下程式碼:

var func = eval('(function(){})');

alert(typeof func); // IE下是undefined

在標準瀏覽器chrome和firefox是列印function,但是在IE下的JScript引擎下列印的是undefined,在這種情況下,我們可以通過具名函式來實現;如下:

eval('function func(){}');

alert(typeof func); // 列印是function

我們使用eval時候,最常見的是ajax請求伺服器端返回一個字串的格式的資料,我們需要把字串的格式的資料轉換為json格式;如下程式碼:

// 比如伺服器返回的資料是如下字串,想轉換成json物件如下:

var data = '{"name":"Mike","sex":"女","age":"29"}';

console.log(eval("("+data+")"));

列印Object {name: "Mike", sex: "女", age: "29"} 就變成了一個物件;

// 或者直接如下 ,都可以

console.log(eval("("+'{"name":"Mike","sex":"女","age":"29"}'+")"));

我們還需要明白的是使用eval或者with語句,他們都會改變作用域的問題,比如使用eval如下程式碼:

var i = 100;
function myFunc(name) {
      console.log('value is:'+i); // 100
      eval(name);
       console.log('value is:'+i); // 10
}    
myFunc('var i = 10;');

如上程式碼,第一次執行的是100,第二次呼叫eval()方法,使作用域變成函式內部了,因此i變成10了;

理解動態方法呼叫(callapply)

Javascript有三種執行體,一種是eval()函式入口引數中指定的字串,該字串總是被作為當前函式上下文中的語句來執行,第二種是new Function(){}中傳入的字串,該字串總是被作為一個全域性的,匿名函式閉包中的語句行被執行;第三種情況執行體就是一個函式,可以通過函式呼叫運算子”()”來執行;除了以上三種之外,我們現在還可以使用call()方法或者apply()方法作為動態方法來執行;如下程式碼:

function foo(name){
    alert("hi:"+name);
}
foo.call(null,'longen'); // 呼叫列印 hi: longen
foo.apply(null,['tugenhua']); // 呼叫列印 hi:tugenhua

call()方法與apply()方法 使用效果是一樣的,都是呼叫函式,只是第二個引數不一樣,apply第二個引數是一個陣列或者arguments;

callapply中理解this的引用

如果我們將一個普通的函式將作為一個物件的方法呼叫的話,比如我現在有一個普通的函式如下程式碼:

function foo(){
    alert(this.name);
}

現在我們下面定義2個物件如下:

var obj1 = {name:'obj1'};

var obj2 = new Object();

obj2.name = 'obj2';

那麼現在我們使用這2個物件來分別呼叫哪個上面的普通函式foo;如下:

foo.call(obj1);

foo.call(obj2);

可以看到,第一次列印的是obj1,第二次列印的是obj2;也就是說第一次的this指標指向與obj這個物件,第二次this指標指向與obj2這個物件;

下面是程式碼:

function foo(){
    alert(this.name);
}
var obj1 = {name:'obj1'};
var obj2 = new Object();
obj2.name = 'obj2';
foo.call(obj1); // obj1
foo.call(obj2); //obj2

我們在方法呼叫中能查詢this引用以得到當前的例項,因此我們也能夠使用的下面的程式碼來傳送this的引用;

比如如下程式碼:

function foo(){
    alert(this.name);
}
function MyObject(){
    this.name = 'myObject';
}
MyObject.prototype.doAction = function(){
    foo.call(this);
}
// 測試
var obj3 = new MyObject();
obj3.doAction();

如上程式碼先例項化MyObject這個物件,得到例項obj3, 然後呼叫例項的doAction這個方法,那麼當前的this指標就指向了obj3這個例項,同時obj3.name = ‘MyObject’;  所以在呼叫foo.call(this)時,this指標指向與obj3這個例項,因此alert(this.name);就彈出myObject;

使用同樣的方法,我們可以傳遞引數,程式碼如下:

function calc_area(w,h) {
    alert(w*h);
}
function Area() {
    this.name = 'MyObject';
}
Area.prototype.doCalc = function(v1,v2){
    calc_area.call(this,v1,v2);
};
var area = new Area();
area.doCalc(10,20);

如上使用了call方法,並且給call方法傳遞了2個引數,但是上面的我們也可以使用apply()方法來呼叫,我們知道apply()方法和call()方法的不同點就是第二個引數,如上call方法的引數是一個一個的傳遞,但是apply的第二引數是一個陣列或者是arguments,但是他們實現的功能是相同的;

function calc_area(w,h) {
    alert(w*h);
}
function Area() {
    this.name = 'MyObject';
}
Area.prototype.doCalc = function(v1,v2){
    //calc_area.call(this,v1,v2);
    calc_area.apply(this,[v1,v2])
};
var area = new Area();
area.doCalc(10,20);

理解javascript物件

Object.defineProperty方法, 該方法是ECMAScript5提供的方法,該方法接收3個引數,屬性所在的物件,需要修改物件中屬性名字,和一個描述符物件;描述符物件的屬性必須是 configurable、enumerable、writable 和value。設定其中的一或多個值,可以修改對應的特性值。

ECMAScript中有2種屬性,資料屬性和訪問器屬性

  1.   資料屬性;

資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。資料屬性有4個描述其行為的特性;

configurable表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。這個特性值預設為true。

enumerable表示能否通過 for-in 迴圈返回屬性。這個特性值預設為true。

writable表示能否修改屬性的值。這個特性值預設為true。

value:  包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置上,這個特性值預設為undefined;

目前標準的瀏覽器支援這個方法,IE8-不支援此方法;

比如我們先定義一個物件person,如下:

var person = {
    name: 'longen'
};

我們可以先alert(person.name); 列印彈出肯定是longen字串;

理解writable屬性;

現在我們使用Object.defineProperty()方法,對person這個物件的name屬性值進行修改,程式碼如下:

alert(person.name); // longen
Object.defineProperty(person, "name", {
    writable: false,
    value: "tugenhua"
});
alert(person.name); // tugenhua
person.name = "Greg"; 
alert(person.name); // tugenhua

如上程式碼,我們writable設定為false的時候,當我們進行修改name屬性的時候,是修改不了的,但是如果我把writable設定為true或者直接刪掉這行程式碼的時候,是可以修改person中name的值的。如下程式碼:

Object.defineProperty(person, "name", {
    writable: false,
    value: "tugenhua"
});
alert(person.name); // tugenhua
person.name = "Greg"; 
alert(person.name); // Greg

理解configurable屬性

繼續如上JS程式碼如下:

var person = {
    name: 'longen'
};
alert(person.name); // longen
Object.defineProperty(person, "name", {
    configurable: false,
    value: "tugenhua"
});
alert(person.name); // tugenhua
delete person.name;
alert(person.name); // tugenhua

當把configurable設定為false的時候,表示是不能通過delete刪除name這個屬性值的,所以上面的最後一個彈窗還會列印出tugenhua這個字串的;

但是如果我把configurable設定為true或者直接不寫這個屬性的話,那麼最後一個person.name彈窗會是undefined,如下程式碼:

var person = {
    name: 'longen'
};
alert(person.name); // longen
Object.defineProperty(person, "name", {
    value: "tugenhua"
});
alert(person.name); // tugenhua
delete person.name;
alert(person.name); // undefined

理解enumerable屬性

Enumerable屬性表示能否通過for-in迴圈中返回資料,預設為true是可以的,如下程式碼:

var person = {
    name: 'longen'
};
Object.defineProperty(person, "name", {
    enumerable: true,
    value: "tugenhua"
});
alert(person.name); // tugenhua
for(var i in person) {
    alert(person[i]); // 可以彈出框
}

如上是把enumerable屬性設定為true,但是如果把它設定為fasle的時候,for-in迴圈內的資料就不會返回資料了,如下程式碼:

var person = {
    name: 'longen'
};
Object.defineProperty(person, "name", {
    enumerable: false,
    value: "tugenhua"
});
alert(person.name); // tugenhua
for(var i in person) {
    alert(person[i]); // 不會彈出
}

2.   訪問器屬性

訪問器屬性有getter和setter函式,在讀取訪問器屬性時,會呼叫getter函式,這個函式負責返回有效的值,在寫入訪問器屬性時,會呼叫setter函式並傳入新值,這個函式負責如何處理資料,訪問器屬性也有以下4個特性:

configurable表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。這個特性值預設為true。

enumerable表示能否通過 for-in 迴圈返回屬性。這個特性值預設為true。

get在讀取屬性時呼叫的函式,預設值為undefined。

set在寫入屬性時呼叫的函式,預設值為undefined。

如下程式碼:

var book = {
    _year: 2004,
    edit: 1
};
Object.defineProperty(book,"year",{
    get: function(){
        return this._year;
    },
    set: function(newValue) {
        if(newValue > 2004) {
            this._year = newValue;
            this.edit += newValue - 2004;
        }
    }
});
book.year = 2005;
alert(book.edit); //2

首先我們先定義一個book物件,有2個屬性_year和edit,並初始化值,給book物件再新增值year為2005,而訪問器屬性year則包含一個getter函式和一個setter函式,因此先會呼叫set函式,把2005傳給newValue,之後this._year就等於2005,this.edit就等於2了;

目前支援Object.defineProperty方法的瀏覽器有IE9+,Firefox4+,safari5+,chrome和Opera12+;

理解定義多個屬性

ECMAScript5定義了一個Object.defineProperties()方法,這個方法可以一次性定義多個屬性,該方法接收2個引數,第一個引數是新增或者修改該屬性的物件,第二個引數是一個物件,該物件的屬性與第一個引數的物件需要新增或者刪除的屬性一一對應;

如下程式碼:

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

如上程式碼;給book物件設定了3個屬性,其中前面兩個會覆蓋原有的book的物件的屬性,三個屬性是新增的;

上面確實是給物件設定了多個屬性了,那麼現在我們如何讀取屬性了?

ECMAScript5給我們提供了方法 Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。該方法接收2個引數,屬性所在的物件和需要讀取描述符的屬性名稱。返回值也是一個物件,如果是訪問器屬性,這個物件的屬性有configurable、enumerable、get 和set;如果是資料屬性,這個物件的屬性有configurable、enumerable、writable 和value。

我們先來看下資料屬性,如下程式碼獲取:

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor);

列印出來如下:

Object {value: 2015, writable: true, enumerable: true, configurable: true}

是一個物件,現在的value變成2015了;

但是如果我們來看下訪問器屬性的話,如下程式碼:

var descriptor = Object.getOwnPropertyDescriptor(book, "year");

console.log(descriptor);

列印如下:

就有如上四個屬性了;

Object.getOwnPropertyDescriptor方法,支援這個方法的瀏覽器有:

IE9+,firefox4+,safari5+,opera12+和chrome;

理解建構函式

function Dog(name,age) {
    this.name = name;
    this.age = age;
    this.say = function(){
        alert(this.name);
    }
}

如上就是一個建構函式,它與普通的函式有如下區別:

  1. 函式名第一個首字母需要大寫,為了區分是建構函式。
  2. 初始化函式的時候需要new下;任何函式,只要它是通過new初始化的,那麼他們就可以把它當做建構函式;

比如如下初始化例項化2個物件:

var dog1 = new Dog("wangwang1",'10');
var dog2 = new Dog("wangwang2",'11');

那麼我們現在可以列印console.log(dog1.say());列印出來肯定是wangwang1,console.log(dog2.say());列印出來是wangwang2;

dog1和dog2分別儲存著Dog的一個不同的例項,且這兩個物件都有一個constructor(建構函式)屬性。該屬性指向Dog,如下程式碼:

alert(dog1.constructor === Dog);  // true
alert(dog2.constructor === Dog);  // true

同時例項化出來的物件都是Object的例項,可以通過instanceof 來檢測如下程式碼:

alert(dog1 instanceof Object);    // true
alert(dog2 instanceof Object);    // true
alert(dog1 instanceof Dog);    // true
alert(dog2 instanceof Dog);    // true

建構函式的缺點:就是每個方法都需要在每個例項上重新建立一遍,比如上面的例項化2個Dog物件,分別為dog1和dog2,dog1和dog2都有一個say方法,但是那個方法不是同一個Function的例項,如下程式碼:

alert(dog1.say === dog2.say);  // false

因此我們需要引入原型模式;原型模式就是要解決一個共享的問題,我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途就是讓所有的例項共享屬性和方法;比如還是上面的程式碼改造成如下:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
dog1.say(); 
var dog2 = new Dog();
dog2.say();
alert(dog1.say === dog2.say); // true

如上列印 dog1.say === dog2.say,他們共享同一個方法;為什麼會是這樣的?

我們可以先來理解下原型物件;

不管什麼時候,只要建立了一個函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向函式的原型物件;比如上面的函式Dog,那麼就會為該函式建立一個Dog.prototype 這麼一個物件,在預設情況下,所有的原型物件都會自動獲得一個constructor(建構函式)屬性.

建構函式的任何一個例項都會指向與該建構函式的原型;比如我們可以通過isPrototypeOf()方法來確定物件是否存在這種關係;如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(Dog.prototype.isPrototypeOf(dog1));//true
console.log(Dog.prototype.isPrototypeOf(dog1));//true

也就是說每個例項內部都有一個指標指向與Dog.prototype;ECMAScript5增加了一個新方法,Object.getPrototypeOf();這個方法可以返回屬性值;還是如上面的程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(Object.getPrototypeOf(dog1) === Dog.prototype); //true
console.log(Object.getPrototypeOf(dog1).name);//wangwang

使用Object.getPrototypeOf()可以方便地取得一個物件的原型,而這在利用原型實現繼承的情況下是非常重要的。

支援這個方法的瀏覽器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。

每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從物件例項本身開始。如果在例項中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜尋指標指向的原型物件,在原型物件中查詢具有給定名字的屬性。如果在原型物件中找到了這個屬性,則返回該屬性的值。

雖然可以通過物件例項訪問儲存在原型中的值,但卻不能通過物件例項重寫原型中的值。如果我們在例項中新增了一個屬性,而該屬性與例項原型中的一個屬性同名,那我們就在例項中建立該屬性,該屬性將會遮蔽原型中的那個屬性。

比如如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var dog2 = new Dog();
dog1.name = "aa";
console.log(dog1.name);// aa
console.log(dog2.name);// wangwang

還是我們剛剛上面說的,物件查詢的方式是查詢2次,先查詢例項中有沒有這個屬性,如果物件的例項有這個屬性的話,直接返回例項中的屬性值,否則的話繼續查詢原型中的屬性值,如果有則返回相對應的值,否則的話返回undefined,如上我們先給dog1的例項一個name屬性,那麼再次查詢的話,那麼查詢的是例項中的name屬性,但是例項dog2查詢的還是原型中的name屬性;但是如果我們需要讓其查詢原型的name屬性的話,我們可以使用delete刪除這個例項中的name屬性;如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var dog2 = new Dog();
dog1.name = "aa";
console.log(dog1.name);// aa
console.log(dog2.name);// wangwang
delete dog1.name;
console.log(dog1.name);// wangwang

但是我們可以使用hasOwnProperty()方法可以檢測一個屬性是存在例項中,還是存在原型中,如下測試程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(dog1.hasOwnProperty("name")); // false
dog1.name = "aa";
console.log(dog1.name); // aa
console.log(dog1.hasOwnProperty("name")); // true
console.log(dog2.name); // wangwang
console.log(dog2.hasOwnProperty("name")); // false
delete dog1.name;
console.log(dog1.name); // wangwang
console.log(dog1.hasOwnProperty("name")); //false

理解原型與in操作符

有2種方式使用in操作符,單獨使用和在for-in迴圈中使用,在單獨使用中,in操作符會在通過物件訪問給定屬性時返回true,不管它是在例項中還是在原型中,比如如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
console.log("name" in dog1); // true
dog1.name = "aa";
console.log("name" in dog1); //true

上面程式碼中,name屬性無論是在例項中還是在原型中,結果都返回true,我們可以通過in和hasOwnProperty()方法來確定屬性是不是在原型當中,我們都知道in不管是在例項中還是在原型中都返回true,而hasOwnProperty()方法是判斷是不是在例項中,如果在例項中返回true,那麼我們取反就不在例項當中了;如下程式碼封裝:

function hasPrototypeProperty(object,attr){
    return !object.hasOwnProperty(attr) && (attr in object);
}

如下測試程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
function hasPrototypeProperty(object,attr){
    return !object.hasOwnProperty(attr) && (attr in object);
}
console.log(hasPrototypeProperty(dog1,'name'));  //true 在原型中
dog1.name = 'aa';
console.log(hasPrototypeProperty(dog1,'name')); //false 在例項中

for-in

在使用for-in迴圈時,返回的是所有能夠通過物件訪問的,可列舉的屬性,其中既包括在例項中的屬性,也包括在原型中的屬性;如果在IE8-下遮蔽了原型中已有的方法,那麼在IE8-下不會有任何反應;如下程式碼:

var obj = {
    toString: function(){
        return "aa";
    }
};
for(var i in obj){
    if(i == "toString") {
        alert(1);
    }
}

如果我把上面的toString改成toString22的話,就可以在IE下列印出1,否則沒有任何執行,那是因為他遮蔽了原型中不可列舉屬性的例項屬性不會在for-in迴圈中返回,因為原型中也有toString這個方法,在IE中,由於其實現認為原型的toString()方法被打上了值為false 的[[Enumerable]]標記,因此應該跳過該屬性,結果我們就不會看到警告框。

要取得物件上所有可列舉的例項屬性,可以使用ECMAScript 5 的Object.keys()方法。這個方法接收一個物件作為引數,返回一個包含所有可列舉屬性的字串陣列。

如下程式碼演示:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog()
var keys = Object.keys(Dog.prototype);
console.log(keys);//["name",'age','say']

如上程式碼;keys將儲存為一個陣列,這個順序是在for-in出現的順序,如果我們想要得到所有例項屬性,無論它是否可列舉,我們可以使用 Object.getOwnPropertyNames()方法,如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
var keys = Object.getOwnPropertyNames(Dog.prototype);
console.log(keys);//["name",'age','say']

Object.keys()和Object.getOwnProperty-Names()方法都可以用來替代for-in 迴圈。支援這兩個方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera

12+和Chrome。

我們接下來再來理解下原型物件的概念;如下程式碼:

function Dog() {};
Dog.prototype = {
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof Dog);    // true
console.log(dog1.constructor == Dog); // false
console.log(dog1.constructor == Object); // true

上面的第三行為什麼會列印false呢?我們知道,每建立一個函式,就會同時建立它的prototype物件。這個物件會自動獲得constructor屬性,我們例項化一個物件的時候,那是因為我們沒有給他指定constructor屬性,預設情況下它會重寫prototype物件,因此constructor屬性也就變成了新物件的constructor屬性了,不再指向Dog函式,如果我們需要讓他還是指向與Dog函式的話,我們可以在Dog.property中新增constructor屬性,如下程式碼:

function Dog() {};
Dog.prototype = {
    constructor: Dog,
    name: 'wangwang',
    age:'11',
    say: function(){
        alert(this.name); //wangwang
    }
}
var dog1 = new Dog();
console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof Dog);    // true
console.log(dog1.constructor == Dog); // true

理解原型的動態性

比如如下程式碼:

function Dog() {};
var dog1 = new Dog();
Dog.prototype.say = function(){
    alert(1);
}
dog1.say(); // 1

我們先例項化一個物件後,再給原型新增一個say方法,再我們使用例項呼叫該方法的時候也可以呼叫的到,這也就是說,例項會先搜尋該say方法,如果沒有搜尋到,那麼它會到原型裡面去搜尋該方法,如果能查詢的到就執行,否則就會報錯,沒有這個方法;

雖然可以隨時為原型新增方法和屬性,且修改的方法和屬效能從例項中表現出來,但是如果重寫整個原型方法那就不行了;如下程式碼:

function Dog() {};
var dog1 = new Dog();
Dog.prototype = {
    constructor: 'Dog',
    name:'dog',
    age:'1',
    say: function(){
        alert(this.age);
    }
}
dog1.say(); // 報錯
var dog1 = new Dog();

例項化一個物件時,會為該例項指向原型的指標,但是如果重寫該原型的話,那麼就會把該物件與原來的那個原型切斷關係,那麼繼續呼叫該方法就會呼叫不到,如上面的程式碼,如果我再在重寫該原型下面繼續例項化該物件Dog,繼續呼叫say方法就正常了;如下程式碼:

function Dog() {};
var dog1 = new Dog();
Dog.prototype = {
    constructor: 'Dog',
    name:'dog',
    age:'1',
    say: function(){
        alert(this.age);
    }
}
//dog1.say(); // 報錯
var dog2 = new Dog();
dog2.say(); // 1

理解原型重寫

我們從上面可知,原型是可以被重寫的,那麼原型重寫後造成的問題就是會改變之前的例項指標指向原來的原型,那也就是說之前的原型假如有繼承等操作的話,通過重寫後的原型也會改變,所以在實際操作的時候要小心點,原型重寫可以使同一個構造器例項出2個不同的例項出來;如下程式碼:

function MyObject(){};
var obj1 = new MyObject();
MyObject.prototype.type = 'myObject';
MyObject.prototype.value = "aa";

var obj2 = new MyObject();
MyObject.prototype = {
    constructor: 'MyObject',
    type: 'Brid',
    value:'bb'
};
var obj3 = new MyObject();
// 顯示物件的屬性
alert(obj1.type); // myObject
alert(obj2.type); // myObject
alert(obj3.type); // Brid

如上程式碼:obj1與obj2兩個例項是指向同一個原型的,obj3通過修改原型後,指向與新的建構函式的原型;如下測試程式碼:

// 顯示例項的關係
alert(obj1 instanceof MyObject); // false
alert(obj2 instanceof MyObject); // false
alert(obj3 instanceof MyObject); // true

我們可能會有誤解,為什麼obj1 與 obj2不是MyObject的例項呢?我們從程式碼中確實可以看到,他們2個例項確實是MyObject的例項,那為什麼現在不是呢?那我們現在再來看看物件例項constructor屬性,如下測試程式碼:

console.log(obj1 instanceof obj1.constructor); // false
console.log(obj1.constructor === MyObject); // true

第一行列印false,可以看出 該物件obj1不是 obj1.constructor構造器,第二行列印true,obj1.constructor的構造器還是指向與MyObject物件;如下三行程式碼:

alert(obj1 instanceof MyObject); // false
console.log(obj1 instanceof obj1.constructor); // false
console.log(obj1.constructor === MyObject); // true

從上面的三行程式碼我們可以總結出,原型被重寫後,obj1.constructor的構造器還是指向與MyObject,但是obj1不是obj1.constructor的構造器的例項,那就是說obj1不是MyObject的例項;

在javascript中,一個構造器的原型可以被重寫,那就意味著之前的一個原型被廢棄,在該構造器例項中:

  1. 舊的例項使用這個被廢棄的原型,並受該原型的影響。
  2. 新建立的例項則使用重寫後的原型,受新原型的影響。

理解構造器重寫

上面我們瞭解了原型被重寫,下面我們來講解下構造器被重寫,繼承待會再來研究,我們先來看看構造器的重寫demo,程式碼如下:

function MyObject(){};
var obj1 = new MyObject();
MyObject = function(){};
var obj2 = new MyObject();
console.log(obj1 instanceof MyObject); // false
console.log(obj2 instanceof MyObject); // true
console.log(obj1 instanceof obj1.constructor); // true

如上程式碼 obj1例項化出來物件被下面的MyObject構造器重寫了,因此obj1不是MyObject的例項,obj2才是MyObject的例項,那obj1為什麼是obj1.constructor的例項呢?說明構造器的重寫不會影響例項的繼承關係。

上面的構造器重寫MyObject不是具名函式,下面我們再來看看具名函式的重寫,程式碼如下:

function MyObject(){};
var obj1 = new MyObject();
function MyObject(){};
var obj2 = new MyObject();
console.log(obj1 instanceof MyObject); // true
console.log(obj2 instanceof MyObject); // true
console.log(obj1 instanceof obj1.constructor); // true

如上程式碼;從上面程式碼結構來看,obj1與obj2是2個不同的MyObject()構造器的例項,但是從邏輯上看,後面的MyObject()構造器其實是覆蓋了前面的構造器,所以obj1與obj2都是第二個MyObject的構造器的例項;

因此上面列印的都是true;

原型物件的缺點:

  1. 省略了建構函式傳遞引數,所有例項在預設情況下都取得相同的屬性值和方法,這並不好,比如我想A例項不需要自己的屬性值,B例項需要有自己的屬性值和自己的方法,那麼原型物件就不能夠滿足需求;
  2. 原型最大的好處就是可以共享屬性和方法,但是假如我給A例項化後新增一個方法後,我不想給B例項化新增對應的方法,但是由於原型都是共享的,所以在B例項後也有A中新增的方法;

對應第二點,我們可以看如下demo:

function Dog(){};
Dog.prototype = {
    constructor: Dog,
    name: 'aa',
    values: ["aa",'bb'],
    say: function(){
        alert(this.name);
    }
}
var dog1 = new Dog();
dog1.values.push("cc");
console.log(dog1.values); // ["aa","bb","cc"]
var dog2 = new Dog();
console.log(dog2.values); // ["aa","bb","cc"]

如上程式碼,我給例項化dog1的values再新增一個值為cc後,那麼原型就變成

[“aa”,”bb”,”cc”]後,如果現在再例項化dog2後,那麼繼續列印dog2.values的值也一樣為[“aa”,”bb”,”cc”];

理解組合使用建構函式模式和原型模式

建構函式模式用於定義例項私有屬性,而原型模式可以定義共享的屬性和方法,可以節省記憶體,同時可以有自己的私有屬性和方法,這種方法模式使用的最廣;比如如下程式碼:

function Dog(name,age){
    this.name = name;
    this.age = age;
    this.values = ["aa",'bb'];
};
Dog.prototype = {
    constructor: Dog,
    say: function(){
        alert(this.name);
    }
}
var dog1 = new Dog("dog1",'12');
dog1.values.push("cc");
console.log(dog1.values); // ["aa","bb","cc"]
var dog2 = new Dog("dog2",'14');
console.log(dog2.values); // ["aa","bb"]
console.log(dog1.values === dog2.values);//false
console.log(dog1.say === dog2.say); //true

還有許多其他的模式,我這邊不一一介紹,需要了解的話,可以看看Javascript設計模式那本書;

理解Javascript繼承

一:原型鏈

ECMAScript中有原型鏈的概念,並將原型鏈作為繼承的主要的方法,其思想是讓一個引用型別去繼承另一個引用型別的屬性和方法,從上面我們瞭解到,原型和例項的關係,每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標,例項與建構函式本身沒有什麼關係,比如我們現在讓一個原型物件等於另一個型別的例項,此時的原型物件將包含一個指向另一個型別的指標,相應的,另一個原型中也包含著指向另一個建構函式的指標,那麼層層遞進,就成了原型鏈;

如下程式碼:

function Animal() {
    this.name = "aa";
}
Animal.prototype.fly = function(){
    return this.name;
};
function Dog() {
    this.value = "bb";
}
Dog.prototype = new Animal();
Dog.prototype.fly = function(){
    return this.name;
};
var dog1 = new Dog();
console.log(dog1.fly()); // aa

如上程式碼:我們先定義了一個Animal這個建構函式,它有一個屬性name=”aa”; 且原型定義了一個方法fly; 接著我定義了Dog這麼一個建構函式,且讓其原型等於Animal的例項,也就是使用這種方式使Dog這個建構函式繼承了Animal的屬性和方法,因此Dog有Animal這個建構函式所有的屬性和方法,接著再定義Dog的自己的fly方法,它會覆蓋原型Animal的方法,且指標還是指向與Animal的,因此this.name =”aa”; 所以當我們例項化Dog的時候,訪問dog1.fly()方法的時候,列印出aa;

如上程式碼我們知道如果想要A繼承與B的話,那麼繼承可以這樣寫:

A.prototype = new B();

還有Dog的fly方法實際上是對原型Animal的fly方法進行重寫;我們繼續看看dog1例項與Dog與Animal的關係;如下程式碼:

console.log(dog1 instanceof Dog); // true
console.log(dog1 instanceof Animal); // true
console.log(dog1 instanceof dog1.constructor); // true
console.log(dog1.constructor === Dog); // false
console.log(dog1.constructor === Animal); // true

如上可以看到,dog1是Dog與Animal的例項,dog1還是指向與dog1.constructor,但是dog1的例項的constructor不再指向與Dog了,而是指向與Animal,這是因為dog1.constructor被重寫了的緣故!

通過原型的繼承,我們看到dog1.fly()方法,會經歷如下幾個搜尋步驟,第一先搜尋該例項有沒有fly這個方法,接著搜尋Dog的原型有沒有這個方法,最後悔搜尋Animal這個prototype這個;最後會繼續看Object中有沒有這個方法,我們都知道所有的物件都是Object的例項,我們可以看下:

console.log(dog1 instanceof Object); //true

所有函式預設的原型都繼承與Object的例項,因此預設原型都有一個內部指標指向與Object.prototype; 那也就是說所有的自定義型別都會繼承與toString()方法和valueOf()方法的根本原因,我們知道測試原型與例項的關係除了可以使用instanceof之外,我們還可以使用isPrototypeOf()方法, 如下程式碼:

console.log(Object.prototype.isPrototypeOf(dog1)); // true
console.log(Dog.prototype.isPrototypeOf(dog1));    // true
console.log(Animal.prototype.isPrototypeOf(dog1)); // true

注意:1. 子型別有時候需要重寫超型別的某個方法,或者需要新增超型別中不存在的某個方法,給原型新增的方法一定要放在替換原型方法之後;如下程式碼:

function Animal() {
    this.name = "aa";
}
Animal.prototype.fly = function(){
    return this.name;
};
function Dog() {
    this.value = "bb";
}
// 繼承Animal
Dog.prototype = new Animal();
// 重寫原型的方法
Dog.prototype.fly = function(){
    return this.name;
};
// 給自身新增新方法
Dog.prototype.cry = function(){
    return false;
};
var dog1 = new Dog();
console.log(dog1.fly()); // aa
console.log(dog1.cry()); // false

2 . 通過原型鏈實現繼承時,不能使用物件字面量建立原型方法,因為這樣會重寫原型鏈;如下程式碼:

function Animal() {
    this.name = "aa";
}
Animal.prototype.fly = function(){
    return this.name;
};
function Dog() {
    this.value = "bb";
}
// 繼承Animal
Dog.prototype = new Animal();
// 重寫原型的方法
Dog.prototype = {
    fly: function(){
        return this.name;
    },
    // 給自身新增新方法
    cry: function(){
        return false;
    }        
};
var dog1 = new Dog();
console.log(dog1.fly()); // undefined
console.log(dog1 instanceof Animal); // false

如上程式碼所示:列印dog1.fly()方法 列印出undefined, 列印 dog1 instanceof Animal 列印false,可知:不能使用物件字面量的方法來實現重寫原型的方法,因為這樣做會切斷與原型Animal的關係,比如現在dog1 不是 Animal的例項,且dog1的例項沒有fly這個方法,因為它現在不是繼承了;

使用原型鏈的缺點如下:

1. 我們都知道原型鏈中所有的屬性和方法都會被所有例項共享,雖然原型可以解決共享的問題,這是他的優點,但也是他的缺點,比如我給A例項新增一個屬性,當我例項化的B的時候,B也有這個屬性,如下程式碼:

function Animal() {
    this.values = ["aa",'bb'];
}
function Dog(){};
Dog.prototype = new Animal();
var dog1 = new Dog();
dog1.values.push("cc"); // 新增cc值
console.log(dog1.values); // [“aa”,”bb”,”cc”];
var dog2 = new Dog();
console.log(dog2.values); // [“aa”,”bb”,”cc”];

2. 在建立子型別的例項中,不能向超型別中的建構函式傳遞引數。

理解借用建構函式

針對上面2點,因此我們需要借用於建構函式;其基本思想是:在子型別建構函式的內部呼叫超型別的建構函式,因此我們可以使用call或者apply的方法來呼叫,如下程式碼:

function Animal() {
    this.values = ["aa",'bb'];
}
function Dog(){    
    // Dog繼承於Animal
    Animal.call(this);
};
var dog1 = new Dog();
dog1.values.push("cc"); // 新增cc值
console.log(dog1.values); // ['aa','bb','cc']

var dog2 = new Dog();
console.log(dog2.values); // ['aa','bb']

如上程式碼:使用call或者apply的方法實現繼承,可以得到自己的副本values,因此第一次列印出[“aa”,’bb’,’cc’] 第二次列印出 [“aa”,’bb’];

我們也可以傳遞引數,程式碼如下:

function Animal(name) {
    this.values = ["aa",'bb'];
    this.name = name;
}
function Dog(){    
    // Dog繼承於Animal
    Animal.call(this,"dog22");
    this.age = 22;
};
var dog1 = new Dog();
console.log(dog1.name); // dog22
console.log(dog1.age);  // 22

但是呢,借用建構函式也有缺點;

借用建構函式的缺點:

  1. 建構函式不能複用;
  2. 在超型別中定義的屬性或者方法,在子型別中是不可見的,結果所有型別都只能使用建構函式的模式;

理解組合繼承

需要解決上面的2個問題,我們可以考慮使用組合繼承的方式來實現,就是指建構函式模式與原型模式組合起來一起使用,其思想就是:使用原型鏈實現對原型的屬性和方法的繼承,而借用建構函式來實現對例項中的屬性的繼承;這樣,既可以通過在原型上定義的方法實現函式的複用,又能保證每個例項都有自己的屬性;如下程式碼:

function Animal(name) {
    this.values = ["aa",'bb'];
    this.name = name;
}
Animal.prototype.sayName = function(){
    return this.name;
}
function Dog(name,age){    
    // Dog繼承屬性
    Animal.call(this,name);
    this.age = age;
};
// 繼承方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.sayAge = function(){
    return this.age;
}
var dog1 = new Dog("dog111",'12');
dog1.values.push("cc");
console.log(dog1.values); // ['aa','bb','cc']
console.log(dog1.sayAge()); // 12
console.log(dog1.sayName()); // dog111
        
var dog2 = new Dog("dog222",'14');
console.log(dog2.values); // ['aa','bb']
console.log(dog2.sayAge()); // 14
console.log(dog2.sayName());// dog222

如上程式碼:Animal建構函式定義了2個屬性,name和values,Animal原型中定義了一個方法sayName; Dog建構函式繼承Animal是傳遞了引數name,然後又定義了自己的age引數,最後將Dog.prototpye = new Animal例項化Animal,讓其Dog繼承與Animal中的方法,這樣的設計使Dog的不同的例項分別有自己的屬性,同時又共有相同的方法,也節省了記憶體;

如上程式碼通過方法繼承後,重寫給Dog的constructor指向與Dog;如下程式碼:

Dog.prototype.constructor = Dog;

所以最後的Dog的例項物件的constructor都指向與Dog,我們可以列印如下:

console.log(dog1.constructor === Dog) // true

如果我們把上面的 Dog.prototype.constructor = Dog 註釋掉的話,那麼

console.log(dog1.constructor === Dog) // false

就返回false了;

理解原型式繼承

其思想是:建立一個臨時性的建構函式,然後將其傳入的物件作為該建構函式的原型,最後返回這個臨時建構函式的一個新例項,如下程式碼演示:

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

我們現在可以做一個demo如下:

var person = {
    name: 'aa',
    firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = object(person);
anthorperson.name = "bb";
anthorperson.firends.push("zhaoliu");
var aperson2 = object(person);
aperson2.name = 'cc';
aperson2.firends.push("longen");    
console.log(person.firends); // ["zhangsan", "lisi", "wangwu", "zhaoliu", "longen"];

這樣的原型繼承是必須有一個物件作為另一個物件的基礎,如果有這麼一個物件的話,可以把它傳遞object()函式;

ECMAScript5中新增Object.create()方法規範了原型式的繼承,這個方法接收2個引數,第一個是用作新物件的原型的物件,第二個引數是可選的,含義是一個新物件定義額外屬性的物件;比如如下程式碼:

var person = {
    name: 'aa',
    firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = Object.create(person);
anthorperson.name = "bb";
anthorperson.firends.push("zhaoliu");
        
var bperson = Object.create(person);
bperson.name = 'longen';
bperson.firends.push("longen");
console.log(person.firends); // ["zhangsan", "lisi", "wangwu", "zhaoliu", "longen"]

Object.create()方法的第二個引數與Object.defineProperties()方法的第二個引數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型物件上的同名屬性。

var person = {
    name: 'aa',
    firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = Object.create(person,{
    name: {
        value: 'bb'
    }
});
console.log(anthorperson.name); //bb

目前支援Object.create()方法的瀏覽器有 IE9+,Firefox4+,Safari5+,Opera12+和chrome;

理解寄生組合式繼承

前面我們理解過組合式繼承,組合式繼承是javascript最常用的繼承模式,不過,它也有缺點,它會呼叫兩次超型別的建構函式,第一次在繼承屬性的時候,呼叫,第二次在繼承方法的時候呼叫,如下程式碼:

function Animal(name) {
    this.values = ["aa",'bb'];
    this.name = name;
}
Animal.prototype.sayName = function(){
    return this.name;
}
function Dog(name,age){    
    // Dog繼承屬性
    Animal.call(this,name);
    this.age = age;
};
// 繼承方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.sayAge = function(){
    return this.age;
}

如上面的繼承屬性;Animal.call(this,name);

和繼承方法Dog.prototype = new Animal();

 當第一次繼承屬性的時候,會繼承Animal中的name和values,當第二次呼叫繼承方法的時候,這次又在新物件中建立了例項屬性name和values,這次建立的屬性會覆蓋之前繼承的屬性;因此我們可以使用寄生組合式繼承;

寄生組合式繼承的思想是:是通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。本質上是使用寄生式繼承來繼承超型別中的原型,然後再將結果指定給子型別的原型,寄生組合式的基本模式如下程式碼:

function inheritPrototype(Dog,Animal) {
    var prototype = object(Animal.prototype);
    prototype.constructor = Dog;
    Dog.prototype = prototype;
}

inheritPrototype該方法接收2個引數,子型別建構函式和超型別建構函式,在函式內部,先建立一個超型別的一個副本,。第二步是為建立的副本新增constructor 屬性,從而彌補因重寫原型而失去的預設的constructor 屬性。

最後一步,將新建立的物件(即副本)賦值給子型別的原型。這樣,我們就可以用呼叫inherit-Prototype()函式的語句,去替換前面例子中為子型別原型賦值的語句了,

如下程式碼演示:

function object(obj) {
    function F() {};
    F.prototype = obj;
    return new F();
}
function inheritPrototype(Dog,Animal) {
    var prototype = object(Animal.prototype);
    prototype.constructor = Dog;
    Dog.prototype = prototype;
}
function Animal(name) {
    this.values = ["aa",'bb'];
    this.name = name;
}
Animal.prototype.sayName = function(){
    return this.name;
}
function Dog(name,age){    
    // Dog繼承屬性
    Animal.call(this,name);
    this.age = age;
};
inheritPrototype(Dog,Animal);
var dog1 = new Dog("wangwang",12);
dog1.values.push("cc");
console.log(dog1.sayName()); // wangwang
console.log(dog1.values); // ["aa", "bb", "cc"]

var dog2 = new Dog("ww2",14);
console.log(dog2.sayName()); // ww2
console.log(dog2.values); // ["aa", "bb"]

如上使用寄生組合繼承只呼叫了一次超型別;這就是他們的優點!

相關文章