在寫面向對象編程思想-設計模式中的js部分的時候發現很多基礎知識不了解的話,是很難真正理解和讀懂js面向對象的代碼。為此,在這里先快速補上。然后繼續我們的面向對象編程思想-設計模式。
什么是鴨子類型
javascript是一門典型的動態類型語言,也就弱類型語言。
那什么是鴨子類型:【如果它走起路來像鴨子,叫起來也是鴨子,那么它就是鴨子】
var 鴨子 = {
走路: function () { },
咕咕咕: function () { }
}
var 鸚鵡 = {
走路: function () { },
咕咕咕: function () { }
}
這只鸚鵡同樣有“走路”和“咕咕咕”的方法,那在js的世界里就可以把它當成鴨子。
可以這樣調用:
var 鴨子們 = [];
鴨子們.push(鴨子);
鴨子們.push(鸚鵡);
for (var i = 0; i < 鴨子們.length; i++) {
鴨子們[i].走路();
}
所以js的世界沒有抽象和接口,但可以約定“我們都是鴨子”。
javascript的面向對象
javascript不僅是直譯式腳本語言、動態類型、弱類型語言、函數為一等公民的語言,它還是基於原型的面向對象語言。面向對象三大特性:封裝、繼承、多態,下面我們用js分別實現。
封裝
var Person = (function () {
var sex = "純爺們";
return {
name: "農碼一生",
getInfo: function () {
console.log("name:" + this.name + ",sex:" + sex);
}
};
})();
雖然老的js語法沒有提供private等關鍵字,但是我們可以利用閉包來實現私有字段,達到封裝的目的。
繼承
- 字面量表示:
var Person = {
name: "農碼一生",
getName: function () {
console.log(this.name);
}
};
var obj = Person;
obj.getName();
- 函數構造器:
var Person = function () {
this.name = "農碼一生";
}
Person.prototype.getName = function () {
console.log(this.name);
}
var obj = function () { };
obj.prototype = new Person();//obj繼承於Person
var o = new obj();
o.getName();//直接調用原型中的getName(類似於C#中的調用父類方法)
多態
對於多態,其實上面的鴨子類型已經表現的很清楚了。
var 鴨子們 = [];
鴨子們.push(鴨子);
鴨子們.push(鸚鵡);
for (var i = 0; i < 鴨子們.length; i++) {
鴨子們[i].走路();//對於鸚鵡來說,它可能是跳着走。對於鴨子來說,它可能左右搖擺着走。這就是多態的表現。
}
對於鸚鵡來說,它可能是跳着走。對於鴨子來說,它可能左右搖擺着走。這就是多態的表現。
原型
什么是原型?在js中是沒有類的,那它怎么創建對象。在C#中我們可以通過new關鍵字實例化一個對象,在js中我們用new關鍵字構造一個原型對象。C#中一切對象繼承於Object,js中一切對象的原型是Object。
var Person = function () {
this.name = "農碼一生";
this.sex = "純爺們";
};
console.log(Person.prototype);
我們很多時候給一個對象添加方法的時候就是寫在原型上,這是為什么?直接寫在對象里會有問題嗎?下面我們試試:
var Person = function () {
this.name = "農碼一生";
this.sex = "純爺們";
this.getInfo = function () {
console.log("name:" + this.name + ",sex:" + this.sex);
}
};
好像並看不出什么問題。其實不然...
我們發現,每次構造出來的對象中的方法都會去開辟一個空間。但是對象的方法都是一樣的,完全沒有必要。 我們可以把方法放入原型。
這樣一來,不過我們構造多少對象,其方法都是公用的(單例的)。
可是為什么會這樣呢?
首先,想想原型這個詞,很形象,原本的模型。我們來看一個繼承的例子:
var Person = function () {
this.name = "農碼一生";
this.sex = "純爺們";
this.getInfo = function () {
console.log("name:" + this.name + ",sex:" + this.sex);
}
};
var Student = function () { };
Student.prototype = new Person();//繼承
var s1 = new Student();
var s2 = new Student();
console.log(s1.getInfo === s2.getInfo);
雖然getInfo在Person里面是直接實現的,但是到了Student的原型(prototype)里面就是一個Person對象的單例了。也就是說無論構造多少個Student對象其中的getInfo方法都是同一個。
但是,構造多個Person就有多個getInfo方法。所以,我們應該把getInfo方法放入Person的原型中。
var Person = function () {
this.name = "農碼一生";
this.sex = "純爺們";
};
Person.prototype.getInfo = function () {
console.log("name:" + this.name + ",sex:" + this.sex);
};
我們仔細推敲下這句話“把getInfo方法放入Person的原型中”,Person的原型是Object,那也就是說getInfo方法放到Object里面去了?
是的,不信請看:
如果原型和原型的原型都實現了同樣的方法呢?我們來猜猜下面會打印哪個版本
var Person = function () {
this.name = "農碼一生";
};
var Student = function () { };
Student.prototype = new Person();//繼承
var stu = new Student();
Student.prototype.getName = function () {
console.log("我的名字:" + this.name);
}
Person.prototype.getName = function () {
console.log("My name is:" + this.name);
}
stu.getName();
如果注釋掉中文版呢?
有沒有覺得特神奇,具體原因我們用圖來回答:
從另個一角度說,如果對象實現了原型中已有的方法那就等效於C#中虛方法重寫了。
this指向
var name = "張三";
var obj = {
name:"李四",
getName: function(){
console.log(this.name);
}
}
obj.getName();
這個結果大家應該沒什么疑問。
接着看下面的:
window.name = "張三";
var obj = {
name:"李四",
getName: function(){
console.log(this.name);
}
}
//obj.getName();
window.func = obj.getName;
window.func();
暈了沒有?沒關系,告訴大家一個簡單實用的方法:方法是被誰“.”出來的,this就指向的誰。
call
"方法是被誰“.”出來的,this就指向的誰",這個口訣不一定適用所有方法。為什么這么說呢?請看下面:
window.name = "張三";
var obj = {
name: "李四",
getName: function () {
console.log(this.name);
}
}
//obj.getName();
window.func = obj.getName;
window.func.call(obj);
雖然還是window點的,但this已經指向了obj。
因為call可以改變this執行。
這個特性非常有用。比如,我們要編寫一個下拉選中事件。
function func() {
console.log("我點擊了" + $(this).find("option:selected").text());
}
$("#element1").change(function () {
func.call(this);
});
$("#element2").change(function () {
func.call(this);
});
在寫func方法的時候不用考慮具體是那個下拉框元素。
apply
apply和call區別不大。
function func(age, sex) {
console.log("name:" + this.name + ",age:" + age + ",sex:" + sex);
}
var obj = {
name: "曉梅"
}
func.call(obj, "18", "妹子");
func.apply(obj,["18","小美女"]);
call和apply第一個參數都是this指向的對象。call第二個和以后的參數對應方法func的參數。而apply的第二個參數是個數組,包含方法的所有參數。
band
function func(age, sex) {
console.log("name:" + this.name + ",age:" + age + ",sex:" + sex);
}
var obj = {
name: "曉梅"
}
var func1 = func.bind(obj, "18", "妹子");
func1();
和apply、call的區別是,只是改變this指向並不執行。且參數傳入方式和call一樣。
js中的閉包
什么是閉包?我的理解是存在不能被回收的變量就是閉包。
最常見最大的一個閉包就是全局變量,定義了就不會被銷毀,除非自動設為null。
而我們平時說的和使用的閉包卻非如此,但同樣會產生不會被銷毀的變量。比如我們之前說的私有變量示例:
var Person = (function () {
var sex = "純爺們";
return {
name: "農碼一生",
getInfo: function () {
console.log("name:" + this.name + ",sex:" + sex);
}
};
})();
之所以說它是閉包,那是因為sex這個字段是永遠不會被銷毀。你想想,如果被銷毀了,那我們調用getInfo的時候豈不是找不到sex字段了。所以不是不會銷毀,而是不能銷毀。
閉包的作用不僅僅是私有化。我們再來一例:
for (var i = 0; i < 10; i++) {
var t = setTimeout(function () {
console.log(i);
}, 100);
}
並不是我們想象的那樣打印0到9。
因為計時器還沒開始循環就執行完了。而此時變量i已經是10。
我們可以通過閉包為每次循環保存一個閉包變量。
for (var i = 0; i < 10; i++) {
(function (i) {
var t = setTimeout(function () {
console.log(i);
}, 100);
})(i);
}
什么是高階函數
“高階函數”名字特牛逼。其實我們在js中經常使用。
還是私有變量的例子:
var Person = (function () {
var sex = "純爺們";
return {
name: "農碼一生",
getInfo: function () {
console.log("name:" + this.name + ",sex:" + sex);
}
};
})();
- 當函數做被return時,那么就是高階函數。
var getInfo = function (callback) {
$.ajax('url', function (data) {
if (typeof callback === 'function') {
callback(data);
}
});
}
getInfo(function (data) {
alert(data.userName);
});
getInfo在執行的時候,傳入的參數是個函數。
- 當函數被當成參數傳遞時,那么這也是高階函數。
本文已同步至索引目錄:《設計模式學習》
【demo】:https://github.com/zhaopeiym/BlogDemoCode
【推薦】:深入理解javascript原型和閉包系列
生命在於運動,靜止是[ ]。