Javascript中幾個(gè)高級語言特性

2018-06-09 16:08 更新

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

作用域

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

最簡單的例子

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

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

更加common的例子

再來一個(gè)更加common的例子,

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

顯然,foo1的結(jié)果是Value,foo2的結(jié)果是DOTA2,這應(yīng)該很容易理解。

有點(diǎn)迷惑的例子

接下來這個(gè)例子將會(huì)讓人感到迷惑,

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

此時(shí),結(jié)果將會(huì)是undefined。

因?yàn)樵诤瘮?shù)foo內(nèi)部的a1將會(huì)覆蓋函數(shù)外部的變量a1,js搜索作用域是按照從內(nèi)到外的,而且當(dāng)執(zhí)行到console.log時(shí),函數(shù)作用域內(nèi)部的a1還尚未被初始化,所以會(huì)輸出undefined。

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

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
}

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

嵌套作用域

接下來這個(gè)例子將會(huì)演示函數(shù)作用域的嵌套,

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

輸出結(jié)果是foo1。這里我在最內(nèi)層的console.log中打印a1,此時(shí),因?yàn)樽顑?nèi)層的作用域中沒有a1的相關(guān)定義,所以會(huì)往上層作用域搜索,得到a1=’foo1’。這里實(shí)際上有一個(gè)嵌套的作用域關(guān)系。

靜態(tài)作用域

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

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

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

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

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

全局作用域

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

其中有一點(diǎn)需要注意,在任何地方?jīng)]有通過var關(guān)鍵字聲明的變量都是定義在全局變量中。其實(shí),在模塊化編程中,應(yīng)該盡量避免使用全局變量,聲明變量時(shí),無論如何都應(yīng)該避免使用var關(guān)鍵字。

閉包

閉包是函數(shù)式編程語言的一大語言特性。w3c上關(guān)于閉包的嚴(yán)格定義如下:由函數(shù)(環(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

最后的結(jié)果是1,2,3。

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

為什么它的上層作用域是closure函數(shù)呢?因?yàn)椋?/p>

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

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

在看下面的這個(gè)例子,

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

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

閉包的應(yīng)用場景

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

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

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

我們可以通過閉包來實(shí)現(xiàn)私有成員,如下,

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();

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

對象

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

創(chuàng)建和訪問

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

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

我們還可以通過構(gòu)造函數(shù)來創(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,他表示被調(diào)用函數(shù)所處的環(huán)境。他的作用就是在一個(gè)函數(shù)內(nèi)部引用調(diào)用它自己。

在Javascript中,任何函數(shù)都是被某個(gè)對象調(diào)用。

applycall

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

看下面的例子,

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

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

bind方法

可以使用bind方法永久的改變函數(shù)的上下文。bind將會(huì)返回一個(gè)函數(shù)引用。

看下面的這個(gè)例子,

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

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

不過有時(shí)候,bind會(huì)有一些讓人迷惑的地方,看下面這個(gè)例子,

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函數(shù)的輸出結(jié)果仍然是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,現(xiàn)在可以看出來,func2中實(shí)際上是以userthis指針調(diào)用了func,但是在func中并沒有使用this。

prototype

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

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上的屬性,會(huì)被不同的實(shí)例會(huì)共享。通過構(gòu)造函數(shù)創(chuàng)建出來的屬性,每一個(gè)實(shí)例都有一份獨(dú)立的副本。

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中,繼承是依靠一套叫做原型鏈的機(jī)制實(shí)現(xiàn)的。
說的通俗一點(diǎn)就是,在繼承的時(shí)候,將父類的實(shí)例對象直接賦值給子類的prototype對象,這樣子類就擁有了父類的全部屬性。子類還可以在自己的prototype對象上增加自己的特殊屬性。

看下面的例子,

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

對象的復(fù)制

Javascript中所有的對象類型的變量都是指向?qū)ο蟮囊?。所以在賦值和傳遞的實(shí)際上都是對象的引用。

在Javascript中,對象的復(fù)制分為淺拷貝深拷貝

下面的示例是淺拷貝,

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']

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

想要做深拷貝,并不是一件容易的事情,因?yàn)槌嘶緮?shù)據(jù)類型,還有多種不同的對象,對象內(nèi)部還有復(fù)雜的結(jié)構(gòu),因此需要用遞歸的方式來實(shí)現(xiàn)。

看下面的例子,

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

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

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

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

當(dāng)我們試圖使用深拷貝來復(fù)制obj1obj2中的任何一個(gè)時(shí),問題就出現(xiàn)了。因?yàn)樯羁截惖淖龇ㄊ怯龅綄ο缶瓦M(jìn)行遞歸復(fù)制,那么結(jié)果只能無限循環(huán)下去。

對于這種情況,簡單的遞歸已經(jīng)無法解決,必須設(shè)計(jì)一套圖論算法,分析對象之間的依賴關(guān)系,建立一個(gè)拓?fù)浣Y(jié)構(gòu)圖,然后分別依次復(fù)制每個(gè)頂點(diǎn),并重新構(gòu)建它們之間的依賴關(guān)系。這已經(jīng)超出了這里的討論范圍,而且在實(shí)際的工程操作中 幾乎不會(huì)遇到這種需求,所以我們就不繼續(xù)討論了。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號