一、 JS面向?qū)ο缶幊?/h2>
1昆咽、 面向?qū)ο蠼榻B
什么是對(duì)象迈套?
Everything is object (萬物皆對(duì)象), JS語言中將一切都視為 對(duì)象
- 對(duì)象是對(duì)概念的具體化體現(xiàn):
一本書狰右、一輛汽車诡宗、一個(gè)人都可以是對(duì)象怀挠,一個(gè)數(shù)據(jù)庫乃摹、一張網(wǎng)頁云茸、一個(gè)與遠(yuǎn)程服務(wù)器的連接也可以是對(duì)象善镰。
當(dāng)實(shí)物被抽象成對(duì)象妹萨,實(shí)物之間的關(guān)系就變成了對(duì)象之間的關(guān)系,從而就可以模擬現(xiàn)實(shí)情況炫欺,針對(duì)對(duì)象進(jìn)行編程乎完。
- 編程中對(duì)象是一個(gè)容器,封裝了屬性(property)和方法(method)
屬性是對(duì)象的狀態(tài)品洛,方法是對(duì)象的行為(完成某種任務(wù))树姨。比如,我們可以把動(dòng)物抽象為animal對(duì)象桥状,使用“屬性”記錄具體是那一種動(dòng)物帽揪,使用“方法”表示動(dòng)物的某種行為(奔跑、捕獵辅斟、休息等等)转晰。
也可以將其簡單理解為:數(shù)據(jù)集或功能集。
ECMAScript-262 把對(duì)象定義為:無序?qū)傩缘募希鋵傩钥梢园局挡樾稀?duì)象或者函數(shù)蔗崎。
嚴(yán)格來講,這就相當(dāng)于說對(duì)象是一組沒有特定順序的值扰藕。對(duì)象的每個(gè)屬性或方法都有一個(gè)名字缓苛,而每個(gè)名字都
映射到一個(gè)值。
2邓深、 面向?qū)ο缶幊?/h3>
面向?qū)ο蟛皇切碌臇|西未桥,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發(fā)效率和可維護(hù)性庐完。
面向?qū)ο缶幊?—— Object Oriented Programming钢属,簡稱 OOP ,是一種編程開發(fā)思想门躯。
它將真實(shí)世界各種復(fù)雜的關(guān)系淆党,抽象為一個(gè)個(gè)對(duì)象,然后由對(duì)象之間的分工與合作讶凉,完成對(duì)真實(shí)世界的模擬染乌。
在面向?qū)ο蟪绦蜷_發(fā)思想中,每一個(gè)對(duì)象都是功能中心懂讯,具有明確分工荷憋,可以完成接受信息、處理數(shù)據(jù)褐望、發(fā)出信息等任務(wù)勒庄。
因此,面向?qū)ο缶幊叹哂徐`活瘫里、代碼可復(fù)用实蔽、高度模塊化等特點(diǎn),容易維護(hù)和開發(fā)谨读,比起由一系列函數(shù)或指令組成的傳統(tǒng)的過程式編程(procedural programming)局装,更適合多人合作的大型軟件項(xiàng)目。
面向?qū)ο笈c面向過程:
- 面向過程就是親力親為劳殖,事無巨細(xì)铐尚,面面俱到,步步緊跟哆姻,有條不紊
- 面向?qū)ο缶褪钦乙粋€(gè)對(duì)象宣增,指揮得結(jié)果
- 面向?qū)ο髮?zhí)行者轉(zhuǎn)變成指揮者
- 面向?qū)ο蟛皇敲嫦蜻^程的替代,而是面向過程的封裝
面向?qū)ο蟮奶匦裕?/p>
- 封裝性
- 繼承性
- 多態(tài)性
擴(kuò)展閱讀:
3填具、 創(chuàng)建對(duì)象
JavaScript 語言的對(duì)象體系统舀,不基于“類” 創(chuàng)建對(duì)象匆骗,是基于構(gòu)造函數(shù)(constructor)和原型鏈(prototype)。
簡單方式創(chuàng)建對(duì)象
我們可以直接通過 new Object()
創(chuàng)建:
var person = new Object();
person.name = 'Jack';
person.age = 18;
person.sayName = function () {
console.log(this.name);
}
字面量方式創(chuàng)建對(duì)象
每次創(chuàng)建通過 new Object()
比較麻煩誉简,所以可以通過它的簡寫形式對(duì)象字面量來創(chuàng)建:
var person = {
name: 'Jack';
age: 18;
sayName: function () {
console.log(this.name);
}
}
對(duì)于上面的寫法固然沒有問題碉就,但是假如我們要生成兩個(gè) person
實(shí)例對(duì)象呢?
var person1 = {
name: 'Jack';
age: 18;
sayName: function () {
console.log(this.name);
}
}
var person2 = {
name: 'Mike';
age: 16;
sayName: function () {
console.log(this.name);
}
}
通過上面的代碼我們不難看出闷串,這樣寫的代碼太過冗余瓮钥,重復(fù)性太高。
簡單方式的改進(jìn):工廠函數(shù)
我們可以寫一個(gè)函數(shù)烹吵,解決代碼重復(fù)問題:
function createPerson (name, age) {
return {
name: name
age: age
sayName: function () {
console.log(this.name);
}
}
}
然后生成實(shí)例對(duì)象:
var p1 = createPerson('Jack', 18);
var p2 = createPerson('Mike', 18);
這樣封裝確實(shí)爽多了碉熄,通過工廠模式我們解決了創(chuàng)建多個(gè)相似對(duì)象代碼冗余的問題,
但是這依然沒有脫離 使用 字面量方式創(chuàng)建對(duì)象 的本質(zhì)肋拔;
null 與Undefined
都表示空锈津,沒有等含義
歷史原因:
JS在最初設(shè)計(jì)時(shí)只有5中數(shù)據(jù)類型(布爾 、字符串凉蜂、浮點(diǎn)型琼梆、整形、對(duì)象)窿吩,完全沒有考慮空的情況茎杂,將空作為對(duì)象的一種;后來感覺不合理纫雁,但是由于代碼量及兼容性問題煌往,僅僅將NULL從概念上拿出來當(dāng)做獨(dú)立的數(shù)據(jù)類,但是語法層面依然是對(duì)象轧邪,對(duì)于其他情況的空刽脖,則新增了undefined數(shù)據(jù)類型,用于區(qū)分null;
在ES6第一版草案中忌愚,將null刪掉了曾棕,但是在發(fā)布正式版式又加上了;
在JS中unll與undefined出現(xiàn)情況的舉例:
null :
對(duì)象不存在 document.getElementById(‘dsf’);
原型鏈的頂層是Object菜循,獲取Object的原型的值就是null
undefined :
變量聲明沒有賦值
函數(shù)調(diào)用沒有返回值
函數(shù)調(diào)用需要實(shí)參,但是沒有傳遞實(shí)參申尤,函數(shù)內(nèi)的形參是undefined
獲取一個(gè)對(duì)象中不存在的屬性
二癌幕、構(gòu)造函數(shù)
1、 構(gòu)造函數(shù)
JavaScript 語言使用構(gòu)造函數(shù)作為對(duì)象的模板昧穿。
所謂 ”構(gòu)造函數(shù)”勺远,就是一個(gè)普通的函數(shù),只不過我們專門用它來生成對(duì)象时鸵,這樣使用的函數(shù)胶逢,就是構(gòu)造函數(shù)厅瞎;
類似與PHP中 class 類
的作用;
它提供模板初坠,描述對(duì)象的基本結(jié)構(gòu)和簸。
一個(gè)構(gòu)造函數(shù),可以生成多個(gè)對(duì)象碟刺,這些對(duì)象都有相同的結(jié)構(gòu)锁保。
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
var p1 = new Person('Jack', 18);
p1.sayName(); // => Jack
var p2 = new Person('Mike', 23)
p2.sayName(); // => Mike
解析 構(gòu)造函數(shù)代碼 的執(zhí)行
在上面的示例中,Person()
函數(shù)取代了 createPerson()
函數(shù)半沽,但是實(shí)現(xiàn)效果是一樣的爽柒。
這是為什么呢?
我們注意到者填,Person()
中的代碼與 createPerson()
有以下幾點(diǎn)不同之處:
- 沒有顯示的創(chuàng)建對(duì)象
- 直接將屬性和方法賦給了
this
- 沒有
return
語句 - 函數(shù)名使用的是大寫的
Person
而要?jiǎng)?chuàng)建 Person
實(shí)例浩村,則必須使用 new
操作符。
以這種方式調(diào)用構(gòu)造函數(shù)會(huì)經(jīng)歷以下 5 個(gè)步驟:
- 創(chuàng)建一個(gè)空對(duì)象占哟,作為將要返回的對(duì)象實(shí)例心墅。
- 將這個(gè)空對(duì)象的原型,指向構(gòu)造函數(shù)的
prototype
屬性重挑。先記住嗓化,后面講
- 將這個(gè)空對(duì)象賦值給函數(shù)內(nèi)部的
this
關(guān)鍵字。 - 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼谬哀。
- 返回新對(duì)象
function Person (name, age) {
// 當(dāng)使用 new 操作符調(diào)用 Person() 的時(shí)候刺覆,實(shí)際上這里會(huì)先創(chuàng)建一個(gè)對(duì)象
// 然后讓內(nèi)部的 this 指向新創(chuàng)建的對(duì)象
// 接下來所有針對(duì) this 的操作實(shí)際上操作的就是剛創(chuàng)建的這個(gè)對(duì)象
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
// 在函數(shù)的結(jié)尾處會(huì)將 this 返回,也就是這個(gè)新對(duì)象
}
構(gòu)造函數(shù)和實(shí)例對(duì)象的關(guān)系
構(gòu)造函數(shù)是根據(jù)具體的事物抽象出來的抽象模板
實(shí)例對(duì)象是根據(jù)抽象的構(gòu)造函數(shù)模板得到的具體實(shí)例對(duì)象
實(shí)例對(duì)象由構(gòu)造函數(shù)而來史煎,一個(gè)構(gòu)造函數(shù)可以生成很多具體的實(shí)例對(duì)象谦屑,而每個(gè)實(shí)例對(duì)象都是獨(dú)一無二的;
每個(gè)對(duì)象都有一個(gè) constructor
屬性篇梭,該屬性指向創(chuàng)建該實(shí)例的構(gòu)造函數(shù)
反推出來氢橙,每一個(gè)對(duì)象都有其構(gòu)造函數(shù)
console.log(p1.constructor === Person); // => true
console.log(p2.constructor === Person); // => true
console.log(p1.constructor === p2.constructor); // => true
因此,我們可以通過實(shí)例對(duì)象的 constructor
屬性判斷實(shí)例和構(gòu)造函數(shù)之間的關(guān)系
注意:這種方式不嚴(yán)謹(jǐn)恬偷,推薦使用 instanceof
操作符悍手,后面學(xué)原型會(huì)解釋為什么
console.log(p1 instanceof Person); // => true
console.log(p2 instanceof Person); // => true
constructor 既可以判斷也可以獲取
instanceof 只能用于判斷
2、 構(gòu)造函數(shù)存在的問題
以構(gòu)造函數(shù)為模板袍患,創(chuàng)建對(duì)象坦康,對(duì)象的屬性和方法都可以在構(gòu)造函數(shù)內(nèi)部定義;
function Cat(name, color) {
this.name = name;
this.color = color;
this.say = function () {
console.log('hello' + this.name, this.color);
};
}
var cat1 = new Cat('貓', '白色');
var cat2 = new Cat('貓', '黑色');
cat1.say();
cat2.say();
在該示例中诡延,從表面上看好像沒什么問題滞欠,但是實(shí)際上這樣做,有一個(gè)很大的弊端肆良。
那就是對(duì)于每一個(gè)實(shí)例對(duì)象筛璧, name
和 say
都是一模一樣的內(nèi)容逸绎,
每一次生成一個(gè)實(shí)例,都必須為重復(fù)的內(nèi)容夭谤,多占用一些內(nèi)存棺牧,如果實(shí)例對(duì)象很多,會(huì)造成極大的內(nèi)存浪費(fèi)沮翔。
對(duì)于這種問題我們可以把需要共享的函數(shù)定義到構(gòu)造函數(shù)外部:
function say(){
console.log('hello' + this.name, this.color);
}
function Cat(name, color) {
this.name = name;
this.color = color;
this.say = say;
}
var cat1 = new Cat('貓', '白色');
var cat2 = new Cat('貓', '黑色');
cat1.say();
cat2.say();
這樣確實(shí)可以了陨帆,但是如果有多個(gè)需要共享的函數(shù)的話就會(huì)造成全局命名空間及變量沖突的問題。
你肯定想到了可以把多個(gè)函數(shù)放到一個(gè)對(duì)象中用來避免全局命名空間沖突的問題:
var s = {
sayhello:function (){
console.log('hello'+this.name,this.color);
},
saycolor:function(){
console.log('hello'+this.color);
}
}
function Cat(name, color) {
this.name = name;
this.color = color;
this.sayhello = s.sayhello;
this.saycolor = s.saycolor;
}
var cat1 = new Cat('貓', '白色');
var cat2 = new Cat('貓', '黑色');
cat1.sayhello();
cat2.saycolor();
至此采蚀,我們利用自己的方式基本上解決了構(gòu)造函數(shù)的內(nèi)存浪費(fèi)問題疲牵。
但是代碼看起來還是那么的格格不入,那有沒有更好的方式呢榆鼠?
三纲爸、 原型
1、 構(gòu)造函數(shù)的 prototype屬性
JavaScript 的每個(gè)對(duì)象都繼承另一個(gè)父級(jí)對(duì)象妆够,父級(jí)對(duì)象稱為 原型 (prototype)對(duì)象识啦。
原型也是一個(gè)對(duì)象,原型對(duì)象上的所有屬性和方法神妹,都能被子對(duì)象 (派生對(duì)象) 共享
通過構(gòu)造函數(shù)生成實(shí)例對(duì)象時(shí)颓哮,會(huì)自動(dòng)為實(shí)例對(duì)象分配原型對(duì)象。
而每一個(gè)構(gòu)造函數(shù)都有一個(gè)prototype屬性鸵荠,這個(gè)屬性就是實(shí)例對(duì)象的原型對(duì)象冕茅。
null沒有自己的原型對(duì)象。
這也就意味著蛹找,我們可以把所有對(duì)象實(shí)例需要共享的屬性和方法直接定義在構(gòu)造函數(shù)的 prototype
屬性上姨伤,
也就是實(shí)例對(duì)象的原型對(duì)象上。
function Cat(color) {
this.color = color;
}
Cat.prototype.name = "貓";
Cat.prototype.sayhello = function(){
console.log('hello' + this.name, this.color);
}
Cat.prototype.saycolor = function (){
console.log('hello' + this.color);
}
var cat1 = new Cat('白色');
var cat2 = new Cat('黑色');
cat1.sayhello();
cat2.saycolor();
這時(shí)所有實(shí)例的 name
屬性和 sayhello()
庸疾、saycolor
方法乍楚,
其實(shí)都是同一個(gè)內(nèi)存地址,指向構(gòu)造函數(shù)的 prototype
屬性届慈,因此就提高了運(yùn)行效率節(jié)省了內(nèi)存空間徒溪。
2、 構(gòu)造函數(shù)金顿、實(shí)例词渤、原型三者之間的關(guān)系
構(gòu)造函數(shù)的prototype屬性,就是由這個(gè)構(gòu)造函數(shù)new出來的所有實(shí)例對(duì)象的 原型對(duì)象
前面已經(jīng)講過串绩,每個(gè)對(duì)象都有一個(gè) constructor
屬性,該屬性指向創(chuàng)建該實(shí)例的構(gòu)造函數(shù)
對(duì)象._proto_ (兩邊都是兩個(gè)下劃線):獲取對(duì)象的原型對(duì)象;
console.log(cat1.__proto__ == Cat.prototype); // true
注意:ES6標(biāo)準(zhǔn)規(guī)定芜壁,_proto_屬性只有瀏覽器才需要部署礁凡,其他環(huán)境可以不部署,因此不建議使用
3高氮、 原型對(duì)象的獲取及修改
上節(jié)可以看到,想要獲取一個(gè)實(shí)例對(duì)象的原型對(duì)象顷牌,有兩種方式:
1:通過實(shí)例對(duì)象的構(gòu)造函數(shù)的prototype屬性獲取: 實(shí)例對(duì)象.constructor.prototype
2:通過實(shí)例對(duì)象的 _proto_ 屬性獲取: 實(shí)例對(duì)象.__proto__
而這兩種方式剪芍,我們都不建議使用:
obj.constructor.prototype
在手動(dòng)改變?cè)蛯?duì)象時(shí),可能會(huì)失效窟蓝。
function P() {};
var p1 = new P();
function C() {};
// 修改構(gòu)造函數(shù)的prototype屬性的值為p1
C.prototype = p1; //也就是說,此后所有有C構(gòu)造函數(shù)得到的對(duì)象的原型對(duì)象都是p1;
var c1 = new C();
console.log(c1.constructor.prototype === p1) // false
推舉設(shè)置獲取實(shí)例對(duì)象的原型的方式:
Object.getPrototypeOf(實(shí)例對(duì)象) 方法返回一個(gè)對(duì)象的原型對(duì)象苟跪。
這是獲取原型對(duì)象的標(biāo)準(zhǔn)方法亲配。
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('貓'); //獲取cat1對(duì)象的原型對(duì)象
var s = Object.getPrototypeOf(cat1);
console.log(s);
Object.setPrototypeOf(實(shí)例對(duì)象,原型對(duì)象) 為現(xiàn)有對(duì)象設(shè)置原型對(duì)象
第一個(gè)是實(shí)例對(duì)象谁帕,第二個(gè)是要設(shè)置成為實(shí)例對(duì)象的原型對(duì)象的對(duì)象
這是設(shè)置原型對(duì)象的標(biāo)準(zhǔn)方法峡继。
function Cat(name) {
this.name = name;
}
var ob = {p:'波斯'};
var cat1 = new Cat('貓');
//設(shè)置cat1的原型對(duì)象為ob
Object.setPrototypeOf(cat1,ob);
console.log(cat1.p);//cat1的原型對(duì)象中有p屬性
console.log(Object.getPrototypeOf(cat1));
console.log(cat1.__proto__);
//注意:如果對(duì)象的原型被改變,不會(huì)影響構(gòu)造函數(shù)獲取的原型的結(jié)果
console.log(Cat.prototype == cat1.__proto__); //false
以上的兩中方法匈挖,都是在ES6新標(biāo)準(zhǔn)中添加的碾牌;
4、 原型及原型鏈
所有對(duì)象都有原型對(duì)象儡循;
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('貓');
console.log(cat1.__proto__.__proto__.__proto__);
而原型對(duì)象中的屬性和方法舶吗,都可以被實(shí)例對(duì)象直接使用;
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí)择膝,都會(huì)執(zhí)行一次搜索誓琼,目標(biāo)是具有給定名字的屬性
- 搜索首先從對(duì)象實(shí)例本身開始
- 如果在實(shí)例中找到了具有給定名字的屬性,則返回該屬性的值
- 如果沒有找到调榄,則繼續(xù)搜索指針指向的原型對(duì)象踊赠,在原型對(duì)象中查找具有給定名字的屬性
- 如果在原型對(duì)象中找到了這個(gè)屬性,則返回該屬性的值
- 如果還是找不到每庆,就到原型的原型去找筐带,依次類推。
- 如果直到最頂層的Object.prototype還是找不到缤灵,則返回undefined伦籍。
而這正是多個(gè)對(duì)象實(shí)例共享原型所保存的屬性和方法的基本原理。
對(duì)象的屬性和方法腮出,有可能是定義在自身內(nèi)帖鸦,也有可能是定義在它的原型對(duì)象上。
由于原型本身也是對(duì)象胚嘲,又有自己的原型作儿,所以形成了一條 原型鏈(prototype chain)。
注意馋劈,不在要原型上形成多層鏈?zhǔn)讲檎夜ッ蹋浅@速M(fèi)資源
5晾嘶、 更簡單的原型語法
我們注意到,前面例子中每添加一個(gè)屬性和方法就要敲一遍 構(gòu)造函數(shù).prototype
娶吞。
為減少不必要的輸入垒迂,更常見的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(gè)原型對(duì)象:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
}
}
在該示例中妒蛇,我們將 Person.prototype
重置到了一個(gè)新的對(duì)象机断。
這樣做的好處就是為 Person.prototype
添加成員簡單了,但是也會(huì)帶來一個(gè)問題绣夺,那就是原型對(duì)象丟失了 constructor
成員(構(gòu)造函數(shù))吏奸。
所以,我們?yōu)榱吮3?constructor
的指向正確乐导,建議的寫法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
// 將這個(gè)對(duì)象的構(gòu)造函數(shù)指向Person
//constructor: Person, // => 手動(dòng)將 constructor 指向正確的構(gòu)造函數(shù)
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + '苦丁,我今年' + this.age + '歲了')
}
}
var p = new Person();
6、 原生對(duì)象的原型
所有構(gòu)造函數(shù)都有prototype屬性物臂;
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- ……
為內(nèi)置對(duì)象擴(kuò)展原型方法:
例:
var ar = [1,5,23,15,5];
function f(){
var minarr = [];
this.forEach(function(v,k){
if(v < 10){
minarr.push(v);
}
})
return minarr;
}
Object.getPrototypeOf(ar).min10 = f;
console.log(ar.min10());//[1, 5, 5]
// 其他數(shù)組對(duì)象也具有相應(yīng)的方法
var a = [1,2,34,7];
console.log(a.min10()); //[1, 2, 7]
這種技術(shù)被稱為猴子補(bǔ)丁旺拉,并且會(huì)破壞封裝。盡管一些流行的框架(如 Prototype.js)在使用該技術(shù)棵磷,但仍然沒有足夠好的理由使用附加的非標(biāo)準(zhǔn)方法來混入內(nèi)置原型蛾狗。
7、 原型對(duì)象的問題及使用建議
性能問題:
在原型鏈上查找屬性時(shí)是比較消耗資源的仪媒,對(duì)性能有副作用沉桌,這在性能要求苛刻的情況下很重要。
另外算吩,試圖訪問不存在的屬性時(shí)會(huì)遍歷整個(gè)原型鏈留凭。
//聲明構(gòu)造函數(shù)Man
function Man(name){
this.name = name;
this.p = function(){
console.log(this.name+'跑');
}
}
var m = new Man('張三');
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('age')); //false
hasOwnProperty
是 JavaScript 中唯一處理屬性并且不會(huì)遍歷原型鏈的方法。
注意:檢查屬性是否undefined
還不夠偎巢。該屬性可能存在蔼夜,但其值恰好設(shè)置為undefined
。
//聲明構(gòu)造函數(shù)Man
function Man(name){
this.name = name;
this.n = undefined;
this.p = function(){
console.log(this.name + '跑');
}
}
var m = new Man('張三');
if(m.n == undefined){
console.log('沒有n屬性')
}
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('n')); //true
四压昼、 繼承
1求冷、 什么是繼承
- 現(xiàn)實(shí)生活中的繼承
- 程序中的繼承
所謂的繼承,其實(shí)就是在子類(子對(duì)象)能夠使用父類(父對(duì)象)中的屬性及方法;
賦予后輩調(diào)用祖輩資源的權(quán)限窍霞,就是繼承匠题;
2、 原型鏈繼承
//聲明構(gòu)造函數(shù)Run
function Run(){
this.p = function(){
console.log(this.name + '跑');
}
}
//聲明構(gòu)造函數(shù)Man
function Man(name){
this.name = name;
}
//設(shè)置構(gòu)造函數(shù)Man的原型為Run,實(shí)現(xiàn)繼承
Man.prototype = new Run();
var m = new Man('張三');
m.p();
但是但金,并不建議使用原型鏈繼承韭山,而且JS 中不止有原型鏈繼承,還有其他的繼承方式,后面會(huì)講到钱磅;
五巩踏、 函數(shù)進(jìn)階
1、 函數(shù)的聲明及調(diào)用
關(guān)鍵字聲明
function f1(){
console.log('f1');
}
表達(dá)式聲明
var f2 = function(){
console.log('f2');
}
這種寫法將一個(gè)匿名函數(shù)賦值給變量续搀。這時(shí),這個(gè)匿名函數(shù)又稱函數(shù)表達(dá)式(Function Expression)
構(gòu)造函數(shù)方式聲明
var add = new Function(
'x',
'y',
'console.log( x + y )'
);
add(1,2);
上面代碼與下面的代碼等同菠净;
// 等同于
function add(x, y) {
console.log( x + y );
}
add(1,2);
因此禁舷,我們只關(guān)注前兩種即可,后一種只做了解毅往,不建議使用牵咙,這種聲明函數(shù)的方式非常不直觀,幾乎無人使用攀唯;
那么洁桌,關(guān)鍵字聲明和表達(dá)式聲明 有什么區(qū)別?
- 關(guān)鍵字聲明必須有名字
- 關(guān)鍵字聲明會(huì)函數(shù)提升侯嘀,在預(yù)解析階段就已創(chuàng)建另凌,聲明前后都可以調(diào)用
- 函數(shù)表達(dá)式類似于變量賦值
- 函數(shù)表達(dá)式?jīng)]有函數(shù)名字
- 函數(shù)表達(dá)式?jīng)]有變量提升,在執(zhí)行階段創(chuàng)建戒幔,必須在表達(dá)式執(zhí)行之后才可以調(diào)用
// 先調(diào)用后聲明
f1();
function f1(){
console.log('f1');
}
//由于“變量提升”吠谢,函數(shù)f被提升到了代碼頭部,也就是在調(diào)用之前已經(jīng)聲明了
//因此可以正常調(diào)用
而表達(dá)式方式:
f();
var f = function (){};
// TypeError: undefined is not a function
上面的代碼等同于下面的形式诗茎。
var f;
f();
f = function () {};
上面代碼第二行工坊,調(diào)用f
的時(shí)候,f
只是被聲明了敢订,還沒有被賦值王污,等于undefined
,所以會(huì)報(bào)錯(cuò)楚午。
因此昭齐,如果同時(shí)采用function
命令和表達(dá)式聲明同一個(gè)函數(shù),最后總是采用表達(dá)式的定義醒叁。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
2司浪、 第一等公民
JavaScript 語言將函數(shù)看作一種 值,與其它值(數(shù)值把沼、字符串啊易、布爾值等等)地位相同。凡是可以使用值的地方饮睬,就能使用函數(shù)租谈。比如,可以把函數(shù)賦值給變量和對(duì)象的屬性,也可以當(dāng)作參數(shù)傳入其他函數(shù)割去,或者作為函數(shù)的結(jié)果返回窟却。函數(shù)只是一個(gè)可以執(zhí)行的值,此外并無特殊之處呻逆。
由于函數(shù)與其他數(shù)據(jù)類型地位平等夸赫,所以在 JavaScript 語言中又稱函數(shù)為 第一等公民。
函數(shù)作為參數(shù)
function eat (callback) {
setTimeout(function () {
console.log('吃完了');
callback()
}, 1000)
}
eat(function () {
console.log('去唱歌');
})
函數(shù)作為返回值
function f1(){
var s = 1;
function f2(){
console.log(s);
}
return f2;
}
var f = f1();
f();// 1
JS中一切皆對(duì)象咖城,函數(shù)也是對(duì)象茬腿,后面還會(huì)講到
3、參數(shù)及返回值
(1) 參數(shù)
形參和實(shí)參
// 函數(shù)內(nèi)部是一個(gè)封閉的環(huán)境宜雀,可以通過參數(shù)的方式切平,把外部的值傳遞給函數(shù)內(nèi)部
// 帶參數(shù)的函數(shù)聲明
function 函數(shù)名(形參1, 形參2, 形參...){
// 函數(shù)體
}
// 帶參數(shù)的函數(shù)調(diào)用
函數(shù)名(實(shí)參1, 實(shí)參2, 實(shí)參3);
解釋:
- 形式參數(shù):在聲明一個(gè)函數(shù)的時(shí)候,為了函數(shù)的功能更加靈活辐董,有些值是固定不了的悴品,對(duì)于這些固定不了的值。我們可以給函數(shù)設(shè)置參數(shù)简烘。這個(gè)參數(shù)沒有具體的值苔严,僅僅起到一個(gè)占位置的作用,我們通常稱之為形式參數(shù)夸研,也叫形參邦蜜。
- 實(shí)際參數(shù):如果函數(shù)在聲明時(shí),設(shè)置了形參亥至,那么在函數(shù)調(diào)用的時(shí)候就需要傳入對(duì)應(yīng)的參數(shù)悼沈,我們把傳入的參數(shù)叫做實(shí)際參數(shù),也叫實(shí)參姐扮。
function fn(a, b) {
console.log(a + b);
}
var x = 5, y = 6;
fn(x,y);
//x,y實(shí)參絮供,有具體的值。函數(shù)執(zhí)行的時(shí)候會(huì)把x,y復(fù)制一份給函數(shù)內(nèi)部的a和b茶敏,函數(shù)內(nèi)部的值是復(fù)制的新值壤靶,無法修改外部的x,y
var f = function (one) {
console.log(one);
}
f(1, 2, 3)
由于 JavaScript 允許函數(shù)有不定數(shù)目的參數(shù),所以需要一種機(jī)制惊搏,可以在函數(shù)體內(nèi)部讀取所有參數(shù)贮乳。
arguments對(duì)象— 函數(shù)的實(shí)參參數(shù)集合
var f = function (one) {
console.log(arguments);
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3);
rest參數(shù)
由于JavaScript函數(shù)允許接收任意個(gè)參數(shù),于是我們就不得不用arguments
來獲取所有參數(shù):
// 只獲取出a,b意外的參數(shù)
function foo(a, b) {
var i, datas = [];
if (arguments.length > 2) {
for (i = 2; i < arguments.length; i++) {
datas.push(arguments[i]);
}
}
console.log('a = ' + a);
console.log('b = ' + b);
console.log(datas);
}
foo(1,2,3,4,5);
為了獲取 除了 已定義參數(shù)a
恬惯、b
之外的參數(shù)向拆,我們不得不循環(huán)arguments
,并且循環(huán)要從索引2
開始以便排除前兩個(gè)參數(shù)酪耳,這種寫法很別扭浓恳,只是為了獲得額外的datas
參數(shù),有沒有更好的方法?
ES6標(biāo)準(zhǔn)引入了rest參數(shù)颈将,上面的函數(shù)可以改寫為:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 結(jié)果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 結(jié)果:
// a = 1
// b = undefined
// Array []
rest參數(shù)只能寫在最后梢夯,前面用...
標(biāo)識(shí),從運(yùn)行結(jié)果可知晴圾,傳入的參數(shù)先綁定a
颂砸、b
,多余的參數(shù)以數(shù)組形式交給變量rest
死姚,所以沾凄,不再需要arguments
我們就獲取了全部參數(shù)。
如果傳入的參數(shù)連正常定義的參數(shù)都沒填滿知允,也不要緊,rest參數(shù)會(huì)接收一個(gè)空數(shù)組(注意不是undefined
)叙谨。
注意因?yàn)閞est參數(shù)是ES6新標(biāo)準(zhǔn)温鸽,所以,請(qǐng)使用新型瀏覽器手负;
(2) 返回值
當(dāng)函數(shù)執(zhí)行完的時(shí)候涤垫,并不是所有時(shí)候都要把結(jié)果打印。我們期望函數(shù)給我一些反饋(比如計(jì)算的結(jié)果返回進(jìn)行后續(xù)的運(yùn)算)竟终,這個(gè)時(shí)候可以讓函數(shù)返回一些東西蝠猬。也就是返回值。函數(shù)通過return返回一個(gè)返回值
//聲明一個(gè)帶返回值的函數(shù)
function 函數(shù)名(形參1, 形參2, 形參...){
//函數(shù)體
return 返回值;
}
//可以通過變量來接收這個(gè)返回值
var 變量 = 函數(shù)名(實(shí)參1, 實(shí)參2, 實(shí)參3);
函數(shù)的調(diào)用結(jié)果就是返回值统捶,因此我們可以直接對(duì)函數(shù)調(diào)用結(jié)果進(jìn)行操作榆芦。
返回值詳解:
如果函數(shù)沒有顯示的使用 return語句 ,那么這個(gè)函數(shù)就沒有任何返回值喘鸟,僅僅是執(zhí)行了而已匆绣;
如果函數(shù)使用 return語句,那么跟在return后面的值什黑,就成了函數(shù)的返回值崎淳;
如果函數(shù)使用 return語句,但是return后面沒有任何值愕把,那么函數(shù)的返回值也是:undefined ,
? 函數(shù)使用return語句后拣凹,這個(gè)函數(shù)會(huì)在執(zhí)行完 return 語句之后停止并立即退出,也就是說return后面的所有其他代碼都不會(huì)再執(zhí)行恨豁。
return后沒有任何內(nèi)容嚣镜,可以當(dāng)做調(diào)試來使用;
(3) 遞歸
執(zhí)行代碼圣絮,查看執(zhí)行結(jié)果:
function fn1 () {
console.log(111)
fn2()
console.log('fn1')
}
function fn2 () {
console.log(222)
fn3()
console.log('fn2')
}
function fn3 () {
console.log(333)
fn4()
console.log('fn3')
}
function fn4 () {
console.log(444)
console.log('fn4')
}
fn1()
/*
** 執(zhí)行結(jié)果為:
111
222
333
444
fn4
fn3
fn2
fn1
*/
最簡單的一句話介紹遞歸:函數(shù)內(nèi)部自己調(diào)用自己
遞歸案例:
計(jì)算 1+2+3+......+100 的結(jié)果
function sum(n){
if(n > 1){
return n + sum(n - 1);
}else{
return 1;
}
}
var jieguo = sum(5);
console.log(jieguo);
遞歸必須要有判斷條件祈惶,不加判斷會(huì)死;
4、 作用域
作用域(scope)指的是變量存在的范圍捧请。在 ES5 的規(guī)范中凡涩,Javascript 只有兩種作用域:一種是全局作用域,變量在整個(gè)程序中一直存在疹蛉,所有地方都可以讀然罨;另一種是函數(shù)作用域(局部作用域)可款,變量只在函數(shù)內(nèi)部育韩。
函數(shù)外部聲明的變量就是全局變量(global variable),它可以在函數(shù)內(nèi)部讀取闺鲸。
var v = 1;
function f() {
console.log(v);
}
f()
// 1
在函數(shù)內(nèi)部定義的變量筋讨,外部無法讀取,稱為“局部變量”(local variable)摸恍。
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
注意悉罕,對(duì)于var
命令來說,局部變量只能在函數(shù)內(nèi)部聲明立镶,在其他區(qū)塊中聲明壁袄,一律都是全局變量。
(1) 作用域鏈
只有函數(shù)可以制造作用域結(jié)構(gòu)媚媒, 那么只要是代碼嗜逻,就至少有一個(gè)作用域, 即全局作用域。凡是代碼中有函數(shù)缭召,那么這個(gè)函數(shù)就構(gòu)成另一個(gè)作用域栈顷。如果函數(shù)中還有函數(shù),那么在這個(gè)作用域中就又可以誕生一個(gè)作用域嵌巷。
將這樣的所有的作用域列出來妨蛹,可以有一個(gè)結(jié)構(gòu): 函數(shù)內(nèi)指向函數(shù)外的鏈?zhǔn)浇Y(jié)構(gòu)。就稱作作用域鏈晴竞。
function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
}
var num = 456;
f1();
5蛙卤、 函數(shù)內(nèi)部的變量聲明的提升
與全局作用域一樣,函數(shù)作用域內(nèi)部也會(huì)產(chǎn)生“變量提升”現(xiàn)象噩死。
var
命令聲明的變量颤难,不管在什么位置,變量聲明都會(huì)被提升到函數(shù)體的頭部已维。
function foo() {
console.log(y);//undefined
var y = 'Bob';
}
foo();
注意: JavaScript引擎自動(dòng)提升了變量y的聲明行嗤,但不會(huì)提升變量y的賦值。
對(duì)于上述foo()函數(shù)垛耳,JavaScript引擎看到的代碼相當(dāng)于:
function foo() {
var y; // 提升變量y的申明
var x = 'Hello, ' + y;
alert(x);
y = 'Bob';
}
6栅屏、 函數(shù)本身的作用域
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
// 討論結(jié)果為什么是 1 飘千?
函數(shù)本身也是一個(gè)值,也有自己的作用域栈雳。
它的作用域與變量一樣护奈,就是其聲明時(shí)所在的作用域,與其運(yùn)行時(shí)所在的作用域無關(guān)哥纫。
總之霉旗,函數(shù)執(zhí)行時(shí)所在的作用域,是定義時(shí)的作用域蛀骇,而不是調(diào)用時(shí)所在的作用域厌秒。
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x);// ReferenceError: a is not defined
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
7、 閉包
(1) 關(guān)于作用域的問題
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
函數(shù)內(nèi)部可以直接讀取全局變量擅憔,函數(shù) f1 可以讀取全局變量 n鸵闪。
但是,在函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量暑诸。
function f1() {
var n = 99;
}
f1()
console.log(n);
有時(shí)我們卻需要在函數(shù)外部訪問函數(shù)內(nèi)部的變量;
正常情況下恋沃,這是辦不到的宜狐,只有通過變通方法才能實(shí)現(xiàn)臭埋。
那就是在函數(shù)的內(nèi)部罢艾,再定義一個(gè)函數(shù)伞矩。
function f1() {
var n = 999;
var f2 = function() {
console.log(n);
}
return f2;
}
var f = f1();
f();
上面代碼中笛洛,函數(shù)f2就在函數(shù)f1內(nèi)部,這時(shí)f1內(nèi)部的所有局部變量乃坤,對(duì)f2都是可見的苛让。
但是反過來就不行,f2內(nèi)部的局部變量湿诊,對(duì)f1就是不可見的狱杰。
這就是JavaScript語言特有的”鏈?zhǔn)阶饔糜颉苯Y(jié)構(gòu)(chain scope),子級(jí)會(huì)一層一層地向上尋找所有父級(jí)的變量厅须。
所以仿畸,父級(jí)的所有變量,對(duì)子級(jí)都是可見的朗和,反之則不成立错沽。
既然f2可以讀取f1的局部變量,那么只要把f2作為返回值眶拉,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
閉包就是函數(shù)f2千埃,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。
由于在JavaScript語言中忆植,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量放可,
因此可以把閉包簡單理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”谒臼。
閉包最大的特點(diǎn),就是它可以“記住”誕生的環(huán)境耀里,比如f2記住了它誕生的環(huán)境f1蜈缤,所以從f2可以得到f1的內(nèi)部變量。
在本質(zhì)上备韧,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁;
閉包(closure)是 Javascript 語言的一個(gè)難點(diǎn)劫樟,也是它的特色,很多高級(jí)應(yīng)用都要依靠閉包實(shí)現(xiàn)织堂。
理解閉包叠艳,首先必須理解變量作用域。
(2)關(guān)于JS垃圾回收機(jī)制的問題
function f1() {
var n = 99;
console.log(++n);
}
f1(); //100
f1(); //100
當(dāng)我們?cè)诤瘮?shù)內(nèi)部引入一個(gè)變量或函數(shù)時(shí)易阳,系統(tǒng)都會(huì)開辟一塊內(nèi)存空間;還會(huì)將這塊內(nèi)存的引用計(jì)數(shù)器進(jìn)行初始化附较,初始化值為0如果外部有全局變量或程序引用了這塊空間,則引用計(jì)數(shù)器會(huì)自動(dòng)進(jìn)行+1操作當(dāng)函數(shù)執(zhí)行完畢后,變量計(jì)數(shù)器重新歸零潦俺,系統(tǒng)會(huì)運(yùn)行垃圾回收拒课,將函數(shù)運(yùn)行產(chǎn)生的數(shù)據(jù)銷毀;如計(jì)數(shù)器不是 0 ,則不會(huì)清楚數(shù)據(jù);
這個(gè)過程就稱之為 "JS的垃圾回收機(jī)制" ;
如果將上節(jié)的代碼事示,改為閉包形式:
function f1() {
var n = 99;
function f2(){
console.log(++n);
}
return f2;
}
var f = f1();
f(); //100
f(); //101
運(yùn)行代碼發(fā)現(xiàn)早像,函數(shù)調(diào)用一次,其變量 n 變化一次;
因 函數(shù)f1被調(diào)用時(shí)肖爵,返回的結(jié)果是f2函數(shù)體卢鹦,也就是說,f2函數(shù)被當(dāng)作值返回給f1的調(diào)用者劝堪,
但是f2函數(shù)并沒有在此時(shí)被調(diào)用執(zhí)行冀自,
所以整個(gè) f1 函數(shù)體,無法判斷子函數(shù)f2會(huì)對(duì)其產(chǎn)生何種影響秒啦,無法判斷 變量n是否會(huì)被使用;
即使f1函數(shù)被調(diào)用結(jié)束熬粗,整個(gè)f1函數(shù)始終保留在內(nèi)存中,不會(huì)被垃圾回收機(jī)制回收;
閉包的最大用處有兩個(gè)余境,一個(gè)是可以讀取函數(shù)內(nèi)部的變量驻呐,另一個(gè)就是讓這些變量始終保持在內(nèi)存中,
即閉包可以使得它誕生環(huán)境一直存在;
注意芳来,外層函數(shù)每次運(yùn)行暴氏,都會(huì)生成一個(gè)新的閉包,而這個(gè)閉包又會(huì)保留外層函數(shù)的內(nèi)部變量绣张,所以內(nèi)存消耗很大答渔。
因此不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題侥涵。
閉包小案例:緩存隨機(jī)數(shù)
需求:獲取隨機(jī)數(shù)沼撕,隨機(jī)數(shù)一旦生成宋雏,會(huì)在程序中多次使用,所以务豺,在多次使用中不變磨总;
function f1(){
var num = parseInt(Math.random() * 100) + 1;
return function (){
console.log(num);
}
}
var f2 = f1();
f2();
f2();
f2();
閉包點(diǎn)贊案例:
<ul>
<li><img src="images/ly.jpg" alt=""><br/><input type="button" value="贊(1)"></li>
<li><img src="images/lyml.jpg" alt=""><br/><input type="button" value="贊(1)"></li>
<li><img src="images/fj.jpg" alt=""><br/><input type="button" value="贊(1)"></li>
<li><img src="images/bd.jpg" alt=""><br/><input type="button" value="贊(1)"></li>
</ul>
<script>
function f1() {
var value=1;
return function () {
this.value="贊("+(++value)+")";
}
}
//獲取所有的按鈕
var btnObjs = document.getElementsByTagName("input");
for(var i = 0; i < btnObjs.length; i++){
var ff = f1();
//將 閉包函數(shù) 綁定給事件
btnObjs[i].onclick = ff;
}
</script>
思考以下兩段代碼的運(yùn)行結(jié)果:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
console.log(this.name);
};
}
};
object.getNameFunc()()
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
console.log(that.name);
};
}
};
object.getNameFunc()();
通過分析代碼得知,其中的 this
是關(guān)鍵笼沥,那么this到底是什么蚪燕?
8、 this 到底是誰
(1) this的不同指向
this
關(guān)鍵字是一個(gè)非常重要的語法點(diǎn)奔浅。毫不夸張地說馆纳,不理解它的含義,大部分開發(fā)任務(wù)都無法完成汹桦。
記住一點(diǎn):this
不管在什么地方使用:它永遠(yuǎn)指向一個(gè)對(duì)象鲁驶。
下面是一個(gè)實(shí)際的例子。
var person = {
name: '張三',
describe: function () {
console.log('姓名:' + this.name);
}
};
person.describe()
// "姓名:張三"
上面代碼中舞骆,this.name
表示name
屬性所在的那個(gè)對(duì)象钥弯。由于this.name
在describe
方法中調(diào)用,而describe
方法所在的當(dāng)前對(duì)象是person
督禽,因此this
指向person
脆霎,this.name
就是person.name
。
由于對(duì)象的屬性可以賦給另一個(gè)對(duì)象狈惫,所以屬性所在的當(dāng)前對(duì)象是可變的睛蛛,即this
的指向是可變的。
var A = {
name: '張三',
describe: function () {
console.log('姓名:' + this.name);
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe()
// "姓名:李四"
上面代碼中虱岂,A.describe
屬性被賦給B
,于是B.describe
就表示describe
方法所在的當(dāng)前對(duì)象B
菠红,所以this.name
就指向B.name
第岖。
稍稍重構(gòu)這個(gè)例子,this
的動(dòng)態(tài)指向就能看得更清楚试溯。
function f() {
console.log('姓名:' + this.name);
}
var A = {
name: '張三',
describe: f
};
var B = {
name: '李四',
describe: f
};
A.describe() // "姓名:張三"
B.describe() // "姓名:李四"
上面代碼中蔑滓,函數(shù)f
內(nèi)部使用了this
關(guān)鍵字,隨著f
所在的對(duì)象不同遇绞,this
的指向也不同键袱。
只要函數(shù)被賦給另一個(gè)變量,this
的指向就會(huì)變摹闽。
var A = {
name: '張三',
describe: function () {
console.log('姓名:' + this.name);
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"
上面代碼中蹄咖,A.describe
被賦值給變量f
,的內(nèi)部this
就會(huì)指向f
運(yùn)行時(shí)所在的對(duì)象(本例是頂層對(duì)象)付鹿。
案例:判斷數(shù)值合法性
<input type="text" name="age" size=3 onChange="validate(this, 10, 20);">
<script>
function validate(obj, x, y){
if ((obj.value < x) || (obj.value > y)){
console.log('合法');
}else{
console.log('不合法');
}
}
</script>
上面代碼是一個(gè)文本輸入框澜汤,每當(dāng)用戶輸入一個(gè)值蚜迅,鼠標(biāo)離開焦點(diǎn)就會(huì)觸發(fā)onChange事件,并執(zhí)行validate
回調(diào)函數(shù)俊抵,驗(yàn)證這個(gè)值是否在指定范圍谁不。瀏覽器會(huì)向回調(diào)函數(shù)傳入當(dāng)前對(duì)象,因此this
就代表傳入當(dāng)前對(duì)象(即文本框)徽诲,就然后可以從this.value
上面讀到用戶輸入的值刹帕。
JavaScript語言之中,一切皆對(duì)象谎替,運(yùn)行環(huán)境也是對(duì)象偷溺,所以函數(shù)都是在某個(gè)對(duì)象下運(yùn)行的,
this
就是函數(shù)運(yùn)行時(shí)所在的對(duì)象(環(huán)境)院喜。這本來并不會(huì)讓我們糊涂亡蓉,但是JavaScript支持運(yùn)行環(huán)境動(dòng)態(tài)切換,也就是說喷舀,this
的指向是動(dòng)態(tài)的砍濒,沒有辦法事先確定到底指向哪個(gè)對(duì)象,這才是最初初學(xué)者感到困惑的地方硫麻。
(2) 使用場合
① 全局環(huán)境
全局環(huán)境使用this
爸邢,它指的就是頂層對(duì)象window
。
this === window // true
function f() {
console.log(this === window);
}
f() // true
② 構(gòu)造函數(shù)
構(gòu)造函數(shù)中的this
拿愧,指的是實(shí)例對(duì)象杠河。
function F(){
this.hello=function(){
console.log('Hello' + this.name);
}
}
var f1 = new F();
f1.name = '張三';
f1.hello();
var f2 = new F();
f2.name = '劉能';
f2.hello();
③ 對(duì)象的方法
方法在哪個(gè)對(duì)象下,this就指向哪個(gè)對(duì)象浇辜。
var o1 = {
s1:'123',
f1:function (){
console.log(this.s1)
}
}
var o2 = {
s1:'456',
f1:o1.f1
}
o2.f1();
(3) 使用this時(shí)的注意事項(xiàng)
① 避免包含多層this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}
f2();
}
}
o.f1()
// Object
// Window
如果要在內(nèi)層函數(shù)中使用外層的this指向券敌,一般的做法是:
var o = {
f1: function () {
console.log(this);
var that = this;
var f2 = function () {
console.log(that);
}
f2();
}
}
o.f1()
// Object
// Object
② 不在循環(huán)數(shù)組中使用this
var ar = ['a', 'b', 'c'];
ar.forEach(function(v, k, ar){
console.log(this[k])
})
this
的動(dòng)態(tài)切換,固然為JavaScript創(chuàng)造了巨大的靈活性柳洋,但也使得編程變得困難和模糊待诅。
有時(shí),需要把this
固定下來熊镣,避免出現(xiàn)意想不到的情況卑雁;JavaScript提供了call
,apply
绪囱,bind
這三個(gè)方法测蹲,來切換/固定this
的指向。
9鬼吵、 call()方法扣甲、apply()方法、bind()方法
call()方法
var lisi = {names:'lisi'};
var zs = {names:'zhangsan'};
function f(age){
console.log(this.names);
console.log(age);
}
f(23);//undefined
f.call(zs,32);//zhangsan
call方法使用的語法規(guī)則
函數(shù)名稱.call(obj,arg1,arg2...argN);
參數(shù)說明:
obj:函數(shù)內(nèi)this要指向的對(duì)象,
arg1,arg2...argN :參數(shù)列表齿椅,參數(shù)與參數(shù)之間使用一個(gè)逗號(hào)隔開
apply()方法
函數(shù)名稱.apply(obj,[arg1,arg2...,argN])
參數(shù)說明:
obj :this要指向的對(duì)象
[arg1,arg2...argN] : 參數(shù)列表文捶,但是要求格式為數(shù)組
var lisi = {name:'lisi'};
var zs = {name:'zhangsan'};
function f(age, sex){
console.log(this.name + age + sex);
}
f.apply(zs, [23, 'nan']);
bind()方法
bind
方法用于將函數(shù)體內(nèi)的this
綁定到具體的某個(gè)對(duì)象上
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下荷逞,"this"指向全局作用域
// 創(chuàng)建一個(gè)新函數(shù),將"this"綁定到module對(duì)象
// 新手可能會(huì)被全局的x變量和module里的屬性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
六粹排、 再談 面向?qū)ο?/h2>
1种远、 對(duì)象
(1) JS的類
在JS中,想要獲取一個(gè)對(duì)象顽耳,有多種方式:
var o1 = {}
var o2 = new Object()
自定義構(gòu)造函數(shù)方式
function Point(x, y) {
this.x = x;
this.y = y;
this.toString = function () {
return this.x + ', ' + this.y;
};
}
var p = new Point(1, 2);
console.log(p.toString());
但是上面這種使用構(gòu)造函數(shù)獲取對(duì)象的寫法跟傳統(tǒng)的面向?qū)ο笳Z言(比如 C++ 和 Java)差異很大坠敷,很容易讓新學(xué)習(xí)這門語言的程序員感到困惑。
ES6 提供了更接近傳統(tǒng)語言的寫法射富,引入了 Class(類)這個(gè)概念膝迎,作為對(duì)象的模板。
通過class
關(guān)鍵字胰耗,可以定義類限次。
基本上,ES6 的class
可以看作只是一個(gè)語法糖柴灯,它的絕大部分功能卖漫,ES5 都可以做到,新的class
寫法只是讓對(duì)象原型的寫法更加清晰赠群、更像面向?qū)ο缶幊痰恼Z法而已羊始。
上面的代碼用 ES6 的class
改寫,就是下面這樣查描。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return this.x + ', ' + this.y ;
}
}
var p = new Point(1, 2);
console.log(p.toString());
上面代碼定義了一個(gè)“類”突委,可以看到里面有一個(gè)constructor
方法,這就是構(gòu)造方法(后面還會(huì)講到)冬三,而this
關(guān)鍵字則代表實(shí)例對(duì)象匀油。也就是說,ES5 的構(gòu)造函數(shù)Point
勾笆,對(duì)應(yīng) ES6 的類Point
敌蚜。
Point
類除了構(gòu)造方法,還定義了一個(gè)toString
方法匠襟。注意钝侠,定義“類”的方法的時(shí)候该园,前面不需要加上function
這個(gè)關(guān)鍵字酸舍,直接把函數(shù)定義放進(jìn)去了就可以了。另外里初,方法之間不需要逗號(hào)分隔啃勉,加了會(huì)報(bào)錯(cuò)。
ES6 的類双妨,完全可以看作構(gòu)造函數(shù)的另一種寫法淮阐。
使用的時(shí)候叮阅,也是直接對(duì)類使用new
命令,跟構(gòu)造函數(shù)的用法完全一致泣特。
類同樣也有prototype
屬性浩姥,而屬性的值依然是實(shí)例對(duì)象的原型對(duì)象;
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return this.x + ', ' + this.y ;
}
}
Point.prototype.toValue = function(){
console.log('123');
}
var p = new Point(1, 2);
p.toValue();
console.log(p.toString());
(2) constructor 方法
constructor
方法是類的默認(rèn)方法状您,通過new
命令生成對(duì)象實(shí)例時(shí)勒叠,自動(dòng)調(diào)用該方法。一個(gè)類必須有constructor
方法膏孟,如果沒有顯式定義眯分,一個(gè)空的constructor
方法會(huì)被默認(rèn)添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
類必須使用new
進(jìn)行實(shí)例化調(diào)用柒桑,否則會(huì)報(bào)錯(cuò)弊决。
而類一旦實(shí)例化,constructor()
方法就會(huì)被執(zhí)行魁淳,就像 人一出生就會(huì)哭一樣飘诗;
constructor
方法默認(rèn)返回實(shí)例對(duì)象(即this
);
但是返回值完全可以指定返回另外一個(gè)對(duì)象先改;
var o1 = {
f1:function(){
console.log('f1');
}
}
class Point{
constructor (){
return o1;
}
f2(){
console.log('f2');
}
}
var p = new Point();
p.f1(); // f1
p.f2(); //Uncaught TypeError: p.f2 is not a function
constructor
方法默認(rèn)返回值盡量不要修改疚察,一旦修改,我們獲取的對(duì)象將脫離其原型仇奶;
(3) 變量提升
我們知道貌嫡,在JS中,不管是全局變量還是局部變量都存在變量提升的特性该溯,函數(shù)也有提升的特性岛抄,也可以先調(diào)用后聲明,構(gòu)造函數(shù)也一樣狈茉;如下面的代碼夫椭,完全沒問題:
var p = new Point();
p.f2();
function Point(){
this.f2 = function(){
console.log('f2');
}
}
但是,需要注意: 類不存在變量提升(hoist)氯庆,這一點(diǎn)與 ES5 完全不同蹭秋。
new Man();
class Man{}
// Man is not defined
注意,class只是在原有面向?qū)ο蟮幕A(chǔ)上新加的關(guān)鍵字而已堤撵,本質(zhì)上依然沒有改變JS依賴原型的面向?qū)ο蠓绞剑?/p>
2仁讨、 再談繼承
(1) 原型鏈繼承的問題
//聲明構(gòu)造函數(shù)Run
function Run(){
this.p = function(){
console.log(this.name + '跑');
}
}
//聲明構(gòu)造函數(shù)Man
function Man(name){
this.name = name;
}
//設(shè)置構(gòu)造函數(shù)Man的原型為Run,實(shí)現(xiàn)繼承
Man.prototype = new Run();
var m = new Man('張三');
m.p();
// 由構(gòu)造函數(shù)獲取原型
console.log(Man.prototype); // 函數(shù)對(duì)象 Run
//標(biāo)準(zhǔn)方法獲取對(duì)象的原型
console.log(Object.getPrototypeOf(m)); // 函數(shù)對(duì)象 Run
//獲取對(duì)象的構(gòu)造函數(shù)
console.log(m.constructor); // Run 函數(shù)
運(yùn)行上面的代碼,我們發(fā)現(xiàn)對(duì)象 m
本來是通過構(gòu)造函數(shù)Man
得到的实昨,可是洞豁,m
對(duì)象丟失了構(gòu)造函數(shù),并且原型鏈繼承的方式,打破了原型鏈的完整性丈挟,不建議使用刁卜;
(2) 冒充方式的繼承
前面我們?cè)趯W(xué)習(xí)JS面向中的面向?qū)ο缶幊虝r(shí),談到了繼承曙咽;
所謂的繼承蛔趴,其實(shí)就是在子類(子對(duì)象)能夠使用父類(父對(duì)象)中的屬性及方法;
function f1(){
this.color = '黑色';
this.h = function(){
console.log(this.color + this.sex);
}
}
function F2(){
this.sex = '鋁';
this.fn = f1;
}
var b = new F2();
b.fn();
b.h();
運(yùn)行以上代碼可知,由構(gòu)造函數(shù)獲取的對(duì)象 b
可以調(diào)用函數(shù)f1中的屬性及方法例朱;
有運(yùn)行結(jié)果可知夺脾,f1
函數(shù)中的this
實(shí)際指向了對(duì)象b
,對(duì)象b
實(shí)際上已經(jīng)繼承了f1
這種方式稱為 對(duì)象冒充方式繼承茉继,ES3之前的代碼中經(jīng)常會(huì)被使用咧叭,但是現(xiàn)在基本不使用了;
為什么不使用了呢烁竭?
還是要回到上面的代碼菲茬,本質(zhì)上講,我們只是改變了函數(shù)f1
中this的指向派撕,
f1中的this指向誰婉弹,誰就會(huì)繼承f1;
而call和apply就是專門用來改變 函數(shù)中this指向的;
call或apply 實(shí)現(xiàn)繼承
function Run(){
this.p = function(){
console.log('ppp');
}
}
function Man(){
//將Run函數(shù)內(nèi)部的this指向Man的實(shí)例化對(duì)象;
Run.call(this);
}
var m = new Man();
m.p();
//獲取對(duì)象的構(gòu)造函數(shù)
console.log(m.constructor); // Man 函數(shù)
// 由構(gòu)造函數(shù)獲取原型
console.log(Man.prototype); // 函數(shù)對(duì)象 Man
//標(biāo)準(zhǔn)方法獲取對(duì)象的原型
console.log(Object.getPrototypeOf(m)); // 函數(shù)對(duì)象 Man
call或apply 實(shí)現(xiàn)的繼承依然是使用對(duì)象冒充方式實(shí)現(xiàn)终吼, 此方式即實(shí)現(xiàn)了繼承的功能镀赌,同時(shí)也不再出現(xiàn)原型鏈繼承中出現(xiàn)的問題;
(3)Object.create() 創(chuàng)建實(shí)例對(duì)象及原型繼承
構(gòu)造函數(shù)作為模板际跪,可以生成實(shí)例對(duì)象商佛。但是,有時(shí)拿不到構(gòu)造函數(shù)姆打,只能拿到一個(gè)現(xiàn)有的對(duì)象良姆。我們希望以這個(gè)現(xiàn)有的對(duì)象作為模板,生成新的實(shí)例對(duì)象幔戏,這時(shí)就可以使用Object.create()
方法玛追。
var person1 = {
name: '張三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2 = Object.create(person1);
person2.name // 張三
person2.greeting() // Hi! I'm 張三.
console.log(Object.getPrototypeOf(person2) == person1); //true
上面代碼中,對(duì)象person1
是person2
的原型闲延,后者繼承了前者所有的屬性和方法痊剖;
Object.create
的本質(zhì)就是創(chuàng)建對(duì)象;
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
//下面三種方式生成的新對(duì)象是等價(jià)的垒玲。
如果想要生成一個(gè)不繼承任何屬性(比如沒有toString
和valueOf
方法)的對(duì)象陆馁,可以將Object.create
的參數(shù)設(shè)為null
。
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
使用
Object.create
方法的時(shí)候侍匙,必須提供對(duì)象原型氮惯,即參數(shù)不能為空,或者不是對(duì)象想暗,否則會(huì)報(bào)錯(cuò)妇汗。
問題:如果想要實(shí)現(xiàn)繼承,應(yīng)該使用哪種繼承方式说莫?
具體對(duì)象實(shí)例繼承使用Object.create
構(gòu)造函數(shù)繼承使用對(duì)象冒充 call杨箭、apply
(4) Class 的繼承
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
}
var m = new Man();
m.p();
Class 可以通過extends
關(guān)鍵字實(shí)現(xiàn)繼承,這比 ES5 的通過修改原型鏈實(shí)現(xiàn)繼承储狭,要清晰和方便很多互婿。
ES5 的繼承,實(shí)質(zhì)是先創(chuàng)造子類的實(shí)例對(duì)象辽狈,然后再將父類的方法添加到上面(Parent.call(this)
)挎挖。
ES6 的繼承機(jī)制完全不同零蓉,是先創(chuàng)造父類的實(shí)例對(duì)象,然后再用子類的構(gòu)造函數(shù)修改。
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
// 顯式調(diào)用構(gòu)造方法
constructor(){}
}
var m = new Man();
m.p();
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived
上面代碼中,Man
繼承了父類Run
蝙茶,但是子類并沒有先實(shí)例化Run
,導(dǎo)致新建實(shí)例時(shí)報(bào)錯(cuò)恨锚。
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
constructor(){
// 調(diào)用父類實(shí)例
super();
}
}
var m = new Man();
m.p();
(5) 繼承總結(jié)
原型鏈繼承:
構(gòu)造函數(shù).prototype --- 會(huì)影響所有的實(shí)例對(duì)象
實(shí)例對(duì)象._proto_ -------- 只會(huì)影響當(dāng)前對(duì)象樟澜,需要重新修改constructor屬性的指向,ES6以后只在瀏覽器下部署
Object.setPrototypeOf(子對(duì)象涮阔,原型) ------ 只會(huì)影響當(dāng)前對(duì)象猜绣,需要重新修改constructor屬性的指向 (ES6的新語法)
空實(shí)例對(duì)象 = Object.create(原型) ;創(chuàng)建對(duì)象并明確指定原型
call 和apply 本質(zhì)上是改變函數(shù)內(nèi)部this 的指向敬特,看起來像繼承掰邢,但實(shí)際上不是繼承
class 的extends 實(shí)現(xiàn)繼承,這是標(biāo)準(zhǔn)繼承方式伟阔,但是只能在ES6中使用
七尸变、 綜合案例
整體思路:
先玩幾次,思考大概的實(shí)現(xiàn)思路减俏;
1:創(chuàng)建基本的靜態(tài)頁面召烂;
2:讓div動(dòng)起來
3:動(dòng)態(tài)創(chuàng)建Div
4:動(dòng)起來后,填補(bǔ)缺失的div
5:隨機(jī)創(chuàng)建黑塊
6:綁定點(diǎn)擊事件
7:點(diǎn)擊判斷輸贏
8:游戲結(jié)束后的限制處理
9:黑塊觸底的處理
10:加分
11:加速
注意變量作用域和this指向的問題
insertBefore娃承、firstChild奏夫、getComputedStyle、appendChild历筝、createElement酗昼、Math.random、Math.floor
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
#cont {
margin-top:100px;
width: 400px;
height: 400px;
border-top:1px solid blue;
position: relative;
/*隱藏頂部*/
overflow: hidden;
}
#main {
width: 400px;
height: 400px;
/*去掉紅框*/
/* border:1px solid red; */
position: relative;
top:-100px;
}
.row {
height: 100px;
}
.row div {
width: 98px;
height: 98px;
border:1px solid gray;
float: left;
}
.black {
background: black;
}
</style>
</head>
<body>
<input type="text" id="fen" value="0" disabled>
<div id="cont">
<div id="main"></div>
</div>
</body>
<script>
function Youxi(){
// 獲取main節(jié)點(diǎn)對(duì)象
this.main = document.getElementById('main');
this.interval = ''; // 定時(shí)器
this.over = false; // 有是否結(jié)束的標(biāo)志
this.sudu = 1; // 初始化下降速度
// 創(chuàng)建DIV的方法
this.cdiv = function(classNames){
// 創(chuàng)建一個(gè)div節(jié)點(diǎn)對(duì)象
var div = document.createElement('div');
// 根據(jù)傳入的值梳猪,創(chuàng)建不同class屬性的div
if(classNames){
div.className = classNames;
}
return div;
}
//一次生成一行div
this.crow = function(init){
var row = this.cdiv('row');
// 獲取0-3的隨機(jī)數(shù)
var k = Math.floor(Math.random() * 4)
// 每行div根據(jù)隨機(jī)數(shù)麻削,隨機(jī)設(shè)置一個(gè)黑塊
for(var i = 0; i < 4; i++){
// 隨機(jī)出現(xiàn)黑塊
if(i == k){
row.appendChild(this.cdiv('black'));
}else{
row.appendChild(this.cdiv());
}
}
return row;
}
// 初始化運(yùn)行
this.init = function(){
// 循環(huán)創(chuàng)建4行蒸痹,并添加到main中
for(var i = 0;i < 4; i++){
var row = this.crow();
this.main.appendChild(row);
}
// 綁定點(diǎn)擊事件
this.clicks();
// 設(shè)置定時(shí)器,使DIV動(dòng)起來
this.interval = window.setInterval('start.move()' , 15);
}
// 綁定點(diǎn)擊事件
this.clicks = function(){
// 因?yàn)樵谄渌饔糜蛑幸褂帽緦?duì)象呛哟,
// 防止this指向沖突叠荠,將this賦值給一個(gè)新的變量,在其他作用域中使用新的變量代替this
var that = this;
// 為整個(gè)main綁定點(diǎn)擊事件
this.main.onclick = function(ev){
// 通過事件對(duì)象扫责,獲取具體點(diǎn)擊的節(jié)點(diǎn)
var focus = ev.target;
// 如果游戲已經(jīng)結(jié)束了
if(that.over){
alert('別掙扎了榛鼎,游戲已經(jīng)結(jié)束了!');
}
// 如果點(diǎn)擊的元素有值為black的class屬性鳖孤,
// 證明用戶點(diǎn)擊的是黑色塊
else if(focus.className == 'black'){
// 獲取文本框節(jié)點(diǎn)對(duì)象
var score = document.getElementById('fen');
// 將文本框的值獲取并加1后重新復(fù)制
var sc = parseInt(score.value)+1;
score.value = sc;
// 將黑塊變白
focus.className = '';
// 如果此行被點(diǎn)擊過者娱,給這一行發(fā)一個(gè)'同行證'
focus.parentNode.pass = true;
// 得分每增加5,下降速度提高0.5個(gè)像素點(diǎn)
if(sc%5 == 0){
that.sudu += 0.5;
}
}else{
// 點(diǎn)擊的不是黑塊苏揣,結(jié)束游戲
window.clearInterval(that.interval);
// 游戲已經(jīng)結(jié)束了
that.over = true;
alert('游戲已結(jié)束')
}
}
}
// 每調(diào)用一次 main 的top值加2像素黄鳍,main就會(huì)向下移動(dòng)2像素
// 我們只需要不斷調(diào)用move,就會(huì)讓main不斷下降
this.move = function(){
// 獲取top值
var t = getComputedStyle(this.main, null)['top'];
var tops = parseInt(t);
// 如果tops大于1,證明一行下降結(jié)束
if(tops>1){
// 如果此行沒有通行證平匈,游戲結(jié)束
if(this.main.lastChild.pass == undefined){
window.clearInterval(this.interval);
// 游戲已經(jīng)結(jié)束了
this.over = true;
alert('游戲已結(jié)束')
}else{ // 如果有通行證
// 如果大于5行际起,刪除最后一行
if(this.main.children.length >= 5) {
this.main.removeChild(this.main.lastChild);
}
}
// 下降結(jié)束一行,則在最頂部增加一行吐葱,完成下降的連續(xù)性
var row = this.crow();
this.main.insertBefore(row,this.main.firstChild);
// 并重新隱藏新加的一行
this.main.style.top = '-100px';
}else{
// 定時(shí)器每調(diào)用一次街望,top 值修改一次
// 完成下降動(dòng)作
this.main.style.top = tops + this.sudu +'px';
}
}
}
var start = new Youxi();
start.init();
</script>
</html>