Javascript中幾個高級語言特性

2018-06-09 16:08 更新

感謝Node.js開發(fā)指南,參考了它的附錄部分內容。

作用域

Javascript中的作用域是通過函數來確定的,這一點與C、Java等靜態(tài)語言有一些不一樣的地方。

最簡單的例子

if (true) {
    var a =  'Value';
}
console.log(a); // Value

上面的代碼片段將會輸出Value。(在瀏覽器環(huán)境中)

更加common的例子

再來一個更加common的例子,

var a1 = 'Valve';
var foo1 = function() {
    console.log(a1);
};
foo1(); // Value
var foo2 = function() {
    var a1 = 'DOTA2';
    console.log(a1);
}
foo2(); // DOTA2

顯然,foo1的結果是Value,foo2的結果是DOTA2,這應該很容易理解。

有點迷惑的例子

接下來這個例子將會讓人感到迷惑,

var a1 = 'mercurial';
var foo = function() {
    console.log(a1);
    var a1 = 'git';
}
foo(); // undefined

此時,結果將會是undefined。

因為在函數foo內部的a1將會覆蓋函數外部的變量a1,js搜索作用域是按照從內到外的,而且當執(zhí)行到console.log時,函數作用域內部的a1還尚未被初始化,所以會輸出undefined。

其實這里還涉及到一個變量懸置的概念,即在Javascript的函數中,無論在何處聲明或者初始化的變量都等效于函數的起始位置聲明,在實際位置賦值。如下,

var foo = function() {
    // do something
    var a = 'ok';
    console.log(a);
    // do something
}

上面這段代碼等效于,

var foo = function() {
    var a; // 注意看這里!
    // do something
    a = 'ok';
    console.log(a);
    // do something
}

最后還有一點需要說明的就是,未定義變量定義但未被初始化的變量,雖然他們的值輸出都是undefined,但是在js內部的實現上還是有區(qū)別的。未定義的變量存在于js的局部語義表上,但是未被分配內存,而定義卻未初始化的變量時實際分配了內存的。

嵌套作用域

接下來這個例子將會演示函數作用域的嵌套,

var foo = function() {
    var a1 = 'foo';
    (function() {
        var a1 = 'foo1';
        (function() {
            console.log(a1);
        })();
    })();
};
foo(); // foo1

輸出結果是foo1。這里我在最內層的console.log中打印a1,此時,因為最內層的作用域中沒有a1的相關定義,所以會往上層作用域搜索,得到a1=’foo1’。這里實際上有一個嵌套的作用域關系。

靜態(tài)作用域

這里還有一點需要注意,就是函數作用的嵌套關系是在定義時就會確定的,而非調用的時候。也即js的作用域是靜態(tài)作用域,好像又叫詞法作用域,因為在代碼做語法分析時就確定下來了??聪旅娴倪@個例子,

var a1 = 'global';
var foo1 = function() {
    console.log(a1);
};
foo1(); // global
var foo2 = function() {
    var a1 = 'locale';
    foo1();
};
foo2(); // global

示例的輸出結果都將會是global。foo1()的執(zhí)行結果為global不需要太多的解釋,很容易明白。

因為foo2在執(zhí)行時,調用foo1,foo1方法會從他自己的作用域開始搜索變量a1,最終在其父級作用域中找到a1,即a1 = 'global'。由此可以看出,foo2內部的foo1在執(zhí)行時并沒有去拿foo2作用域中的變量a1。

以說作用域的嵌套關系并不是在執(zhí)行時確定的,而是在定義時就確定好了的!

全局作用域

最后提一下全局作用域。通過字面的意思就能知道,全局作用域中的變量也好,屬性也好,在任何函數中都能直接訪問。

其中有一點需要注意,在任何地方沒有通過var關鍵字聲明的變量都是定義在全局變量中。其實,在模塊化編程中,應該盡量避免使用全局變量,聲明變量時,無論如何都應該避免使用var關鍵字。

閉包

閉包是函數式編程語言的一大語言特性。w3c上關于閉包的嚴格定義如下:由函數(環(huán)境)及其封閉的自由變量組成的集合體。這句話比較晦澀難懂,反正剛開始我是沒看懂。下面通過一些例子來說明。

閉包解釋

var closure = function() {
    var count = 0;
    return function() {
        count ++;
        return count;
    };
};
var counter = closure();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

最后的結果是1,2,3。

這個demo中,closure是一個函數(其實他相當于一個類的構造函數),并且返回一個函數(這個被返回的函數加上其定義環(huán)境通俗上被稱為閉包)。
在返回的函數中,引用了外部的count變量。在var counter = closure();這句代碼之后,counter實際上就是一個函數,這樣每次在counter()時,先將count自增然后打印出來。
這里counter的函數內部并沒有關于count的定義,所以在執(zhí)行時會往上層作用域搜索,而他的上層作用域是closure函數,而不是counter()執(zhí)行時所在的上層作用域。

為什么它的上層作用域是closure函數呢?因為,

  • 第一,這是在定義的時候就已經確定好的函數作用域嵌套關系,
  • 更重要的是第二點,閉包的返回不但有函數而且還包含定義函數的上下文環(huán)境。這里上下文環(huán)境就是closure函數的內部作用域,所以能夠拿到closure函數中的count變量。

從這里可以看出,閉包會造成對原作用域和其上層作用域的持續(xù)引用。在這里,count變量持續(xù)被引用,其所占用的內存就不會被釋放掉。

在看下面的這個例子,

var closure = function() {
    var count = 0;
    return function() {
        count ++;
        return count;
    };
};
var counter1 = closure();
var counter2 = closure();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
console.log(counter2()); // 2
console.log(counter1()); // 3

從結果可以看出,生成的閉包實例是各自獨立的,他們內部引用的count變量分別屬于各自不同的運行環(huán)境。
我們可以這樣理解,在閉包生成時,將原上下文環(huán)境做了一份拷貝副本,這樣不同的閉包實例就有自己獨立的運行環(huán)境了。

閉包的應用場景

閉包目前來說有兩大用處,

  • 第一是嵌套的回調函數
  • 第二是隱藏對象的部分細節(jié)
$('#id0').animate({
    left: '+50px'
}, 1000, function() {
    $('#id1').animate({
        left: '+50px'
    }, 1000, function() {
        $('#id2').animate({
            left: '+50px'
        }, 1000, function() {
            alert('done');
        });
    });
});

Javascript的對象沒有私有成員的概念。一般的編碼規(guī)范中會要求類似_privateProp的形式來定義私有屬性。但是這是一個非正式的約定,而且_privateProp仍然能夠被訪問到。

我們可以通過閉包來實現私有成員,如下,

var student = function(yourName, yourAge) {
    var name, age;
    name = yourName || '';
    age = yourAge || 0;
    return {
        getName: function() {
            return name;
        },
        getAge: function() {
            return age;
        },
        setName: function(yourName) {
            name = yourName;
        },
        setAge: function(yourAge) {
            age = yourAge;
        }
    };
}
var mamamiya = student('mamamiya', 23);
mamamiya.getName();
mamamiya.getAge();

這里我封裝了一個student類,并設置了兩個屬性name,age。這兩個屬性除了通過student對象的訪問器方法訪問之外,絕無其他的方法能夠訪問到。這里就實現了對部分屬性的隱藏。

對象

Javascript的對象是基于原型的,和其他的一些面向對象語言有一些區(qū)別。

創(chuàng)建和訪問

我們可以通過如下的這種形式來創(chuàng)建一個js對象。

var foo = {
    'a': 'baz',
    'b': 'foz',
    'c': function() {
        return 'hello js';
    }
};

我們還可以通過構造函數來創(chuàng)建對象。

function user(name, uri) {
    this.name = name;
    this.uri = uri;
    this.show = function() {
        console.log(this.name);
    }
};
var mamamiya = new user('mamamiya', 'http://blog.gejiawen.com');
mamamiya.show();

Javascript中上下文對象就是this,他表示被調用函數所處的環(huán)境。他的作用就是在一個函數內部引用調用它自己。

在Javascript中,任何函數都是被某個對象調用。

applycall

在Javascript中applycall是兩個神奇的方法,他們的作用是以不同的上下文環(huán)境來調用函數。通俗點就是說,一個對象可以調用另一個對象的方法。

看下面的例子,

var user = {
    name: 'mamamiya',
    show: function(words) {
        console.log(this.name + ' says ' + words);
    }
};
var foo = {
    name: 'baz'
};
user.show.call(foo, 'hello'); // baz says hello

這段代碼的結果是baz says hello。這里通過call方法改變了user.show方法的上下文環(huán)境,user.show方法在執(zhí)行時,內部的this指向的是foo對象。

bind方法

可以使用bind方法永久的改變函數的上下文。bind將會返回一個函數引用。

看下面的這個例子,

var user = {
    name: 'mamamiya',
    func: function() {
        console.log(this.name);
    }
};
var foo = {
    name: 'baz'
};
foo.func = user.func;
foo.func(); //baz
foo.func1 = user.func.bind(user);
foo.func1(); //mamamiya
func = user.func.bind(foo);
func(); //baz
func2 = func;
func2(); //baz

其實,bind還可以在綁定上下文時附帶一些參數。

不過有時候,bind會有一些讓人迷惑的地方,看下面這個例子,

var user = {
    name: 'mamamiya',
    func: function() {
        console.log(this.name);
    }
};
var foo = {
    name: 'baz'
};
func  = user.func.bind(foo);
func(); //baz
func2 = func.bind(user);
func2(); //baz

這里為什么func2函數的輸出結果仍然是baz呢?

也就是說,我企圖將func的上下文環(huán)境還原到user上為什么沒有起作用?

我們這樣來看,

func = user.func.bind(foo) ≈ function() {
    return user.func.call(foo);
};
func2 = func.bind(user) = function() {
    return func.call(user);
};

ok,現在可以看出來,func2中實際上是以userthis指針調用了func,但是在func中并沒有使用this。

prototype

通過構造函數和原型都能生成對象,但是兩者之間有一些區(qū)別??聪旅娴倪@個列子,

function Class() {
    var a = 'hello';
    this.prop1 = 'git';
    this.func1 = function() {
        a = '';
    };
}
Class.prototype.prop2 = 'Mercurial';
Class.prototype.func2 = function() {
    console.log(this.prop2);
};
var class1 = new Class();
var class2 = new Class();
console.log(class1.func1 === class2.func1); //false
console.log(class1.func2 === class2.func2); //true

所以說,掛在prototype上的屬性,會被不同的實例會共享。通過構造函數創(chuàng)建出來的屬性,每一個實例都有一份獨立的副本。

原型及基于原型的面向對象

那么,什么叫原型鏈?

JavaScript中有兩個特殊的對象:ObjectFunction,它們都是構造函數,用于生成對象。
Object.prototype是所有對象的祖先,Function.prototype是所有函數的原型,包括構造函數。

我把JavaScript中的對象分為三類,

  • 一類是用戶創(chuàng)建的對象,即一般意義上用new語句顯式構造的對象
  • 一類是構造函數對象,即普通的構造函數,通過new調用生成普通對象的函數
  • 一類是原型對象,即構造函數prototype屬性指向的對象

這三類對象中每一種都有一個__proto__屬性,它指向該對象的原型。任何對象沿著它開始遍歷都可以追溯到Object.prototype。

構造函數對象有prototype屬性,指向一個原型對象,通過該構造函數創(chuàng)建對象時,被創(chuàng)建對象的__proto__屬性將會指向構造函數的prototype屬性。原型對象有constructor屬性,指向它對應的構造函數。

看下面的這個例子,幫助理解,

function foo() { }
Object.prototype.name = 'My Object';
foo.prototype.name = 'baz';
var obj = new Object();
var foo = new foo();
console.log(obj.name); // My Object
console.log(foo.name); // baz
console.log(foo.__proto__.name); // baz
console.log(foo.__proto__.__proto__.name); // My Object
console.log(foo.__proto__.constructor.prototype.name); // baz

在Javascript中,繼承是依靠一套叫做原型鏈的機制實現的。
說的通俗一點就是,在繼承的時候,將父類的實例對象直接賦值給子類的prototype對象,這樣子類就擁有了父類的全部屬性。子類還可以在自己的prototype對象上增加自己的特殊屬性。

看下面的例子,

function ClassA() { }
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() { }
ClassB.prototype = new ClassA();

對象的復制

Javascript中所有的對象類型的變量都是指向對象的引用。所以在賦值和傳遞的實際上都是對象的引用。

在Javascript中,對象的復制分為淺拷貝深拷貝

下面的示例是淺拷貝,

Object.prototype.makeCopy = funciton() {
    var newObj = {};
    for (var i in this) {
        newObj[i] = this[i];
    }
    return newObj;
};
var obj = {
    name: 'mamamiya',
    likes: ['js']
};
var newObj = obj.makeCopy();
obj.likes.push('python');
console.log(obj.likes); // ['js', 'python']
console.log(newObj.likes); // ['js', 'python']

從上面的代碼可以看出,淺拷貝只是復制了一些基本屬性,但是對象類型的屬性是被共享的。obj.likesnewObj.likes都指向同一個數組。

想要做深拷貝,并不是一件容易的事情,因為除了基本數據類型,還有多種不同的對象,對象內部還有復雜的結構,因此需要用遞歸的方式來實現。

看下面的例子,

Object.prototype.makeDeepCopy = function() {
    var newObj = {};
    for (var i in this) {
        if (typeof(this[i]) === 'object' || typeof(this[i]) === 'function') {
            newObj[i] = this[i].makeDeepCopy();
        } else {
            newObj[i] = this[i];
        }
    }
    return newObj;
};
Array.prototype.makeDeepCopy = function() {
    var newArray = [];
    for (var i = 0; i < this.length; i++) {
        if (typeof(this[i]) === 'object' || typeof(this[i]) === 'function') {
            newArray[i] = this[i].makeDeepCopy();
        } else {
            newArray[i] = this[i];
        }
    }
    return newArray;
};
Function.prototype.makeDeepCopy = function() {
    var self = this;
    var newFunc = function() {
        return self.apply(this, arguments);
    }
    for (var i in this) {
        newFunc[i] = this[i];
    }
    return newFunc;
};
var obj = {
    name: 'mamamiya',
    likes: ['js'],
    show: function() {
        console.log(this.name);
    }
};
var newObj = obj.makeDeepCopy();
newObj.likes.push('python');
console.log(obj.likes); // ['js']
console.log(newObj.likes); // ['js', 'python']
console.log(newObj.show == obj.show); // false

上面的示例代碼中很好的實現了對象,函數,數組在做深拷貝的邏輯。在一般情況下都是比較好用的。但是有一種情況下,這種方法卻無能為力。如下:

var obj1 = {
    ref: null
};
var obj2 = {
    ref: obj1
};
obj1.ref = obj2;

上面這段代碼塊的邏輯很簡單,就是兩個相互引用的對象。

當我們試圖使用深拷貝來復制obj1obj2中的任何一個時,問題就出現了。因為深拷貝的做法是遇到對象就進行遞歸復制,那么結果只能無限循環(huán)下去。

對于這種情況,簡單的遞歸已經無法解決,必須設計一套圖論算法,分析對象之間的依賴關系,建立一個拓撲結構圖,然后分別依次復制每個頂點,并重新構建它們之間的依賴關系。這已經超出了這里的討論范圍,而且在實際的工程操作中 幾乎不會遇到這種需求,所以我們就不繼續(xù)討論了。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號