一、 JS面向對象編程
1、 面向對象介紹
什么是對象?
Everything is object (萬物皆對象), JS語言中將一切都視為 對象
- 對象是對概念的具體化體現:
一本書、一輛汽車、一個人都可以是對象,一個數據庫、一張網頁、一個與遠程服務器的連接也可以是對象。
當實物被抽象成對象,實物之間的關系就變成了對象之間的關系,從而就可以模擬現實情況,針對對象進行編程。
- 編程中對象是一個容器,封裝了屬性(property)和方法(method)
屬性是對象的狀態,方法是對象的行為(完成某種任務)。比如,我們可以把動物抽象為animal對象,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。
也可以將其簡單理解為:數據集或功能集。
ECMAScript-262 把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。
嚴格來講,這就相當于說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都
映射到一個值。
2、 面向對象編程
面向對象不是新的東西,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發效率和可維護性。
面向對象編程 —— Object Oriented Programming,簡稱 OOP ,是一種編程開發思想。
它將真實世界各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。
在面向對象程序開發思想中,每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。
因此,面向對象編程具有靈活、代碼可復用、高度模塊化等特點,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合作的大型軟件項目。
面向對象與面向過程:
- 面向過程就是親力親為,事無巨細,面面俱到,步步緊跟,有條不紊
- 面向對象就是找一個對象,指揮得結果
- 面向對象將執行者轉變成指揮者
- 面向對象不是面向過程的替代,而是面向過程的封裝
面向對象的特性:
- 封裝性
- 繼承性
- 多態性
擴展閱讀:
3、 創建對象
JavaScript 語言的對象體系,不基于“類” 創建對象,是基于構造函數(constructor)和原型鏈(prototype)。
簡單方式創建對象
我們可以直接通過 new Object()
創建:
var person = new Object();
person.name = 'Jack';
person.age = 18;
person.sayName = function () {
console.log(this.name);
}
字面量方式創建對象
每次創建通過 new Object()
比較麻煩,所以可以通過它的簡寫形式對象字面量來創建:
var person = {
name: 'Jack';
age: 18;
sayName: function () {
console.log(this.name);
}
}
對于上面的寫法固然沒有問題,但是假如我們要生成兩個 person
實例對象呢?
var person1 = {
name: 'Jack';
age: 18;
sayName: function () {
console.log(this.name);
}
}
var person2 = {
name: 'Mike';
age: 16;
sayName: function () {
console.log(this.name);
}
}
通過上面的代碼我們不難看出,這樣寫的代碼太過冗余,重復性太高。
簡單方式的改進:工廠函數
我們可以寫一個函數,解決代碼重復問題:
function createPerson (name, age) {
return {
name: name
age: age
sayName: function () {
console.log(this.name);
}
}
}
然后生成實例對象:
var p1 = createPerson('Jack', 18);
var p2 = createPerson('Mike', 18);
這樣封裝確實爽多了,通過工廠模式我們解決了創建多個相似對象代碼冗余的問題,
但是這依然沒有脫離 使用 字面量方式創建對象 的本質;
null 與Undefined
都表示空,沒有等含義
歷史原因:
JS在最初設計時只有5中數據類型(布爾 、字符串、浮點型、整形、對象),完全沒有考慮空的情況,將空作為對象的一種;后來感覺不合理,但是由于代碼量及兼容性問題,僅僅將NULL從概念上拿出來當做獨立的數據類,但是語法層面依然是對象,對于其他情況的空,則新增了undefined數據類型,用于區分null;
在ES6第一版草案中,將null刪掉了,但是在發布正式版式又加上了;
在JS中unll與undefined出現情況的舉例:
null :
對象不存在 document.getElementById(‘dsf’);
原型鏈的頂層是Object,獲取Object的原型的值就是null
undefined :
變量聲明沒有賦值
函數調用沒有返回值
函數調用需要實參,但是沒有傳遞實參,函數內的形參是undefined
獲取一個對象中不存在的屬性
二、構造函數
1、 構造函數
JavaScript 語言使用構造函數作為對象的模板。
所謂 ”構造函數”,就是一個普通的函數,只不過我們專門用它來生成對象,這樣使用的函數,就是構造函數;
類似與PHP中 class 類
的作用;
它提供模板,描述對象的基本結構。
一個構造函數,可以生成多個對象,這些對象都有相同的結構。
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
解析 構造函數代碼 的執行
在上面的示例中,Person()
函數取代了 createPerson()
函數,但是實現效果是一樣的。
這是為什么呢?
我們注意到,Person()
中的代碼與 createPerson()
有以下幾點不同之處:
- 沒有顯示的創建對象
- 直接將屬性和方法賦給了
this
- 沒有
return
語句 - 函數名使用的是大寫的
Person
而要創建 Person
實例,則必須使用 new
操作符。
以這種方式調用構造函數會經歷以下 5 個步驟:
- 創建一個空對象,作為將要返回的對象實例。
- 將這個空對象的原型,指向構造函數的
prototype
屬性。先記住,后面講
- 將這個空對象賦值給函數內部的
this
關鍵字。 - 執行構造函數內部的代碼。
- 返回新對象
function Person (name, age) {
// 當使用 new 操作符調用 Person() 的時候,實際上這里會先創建一個對象
// 然后讓內部的 this 指向新創建的對象
// 接下來所有針對 this 的操作實際上操作的就是剛創建的這個對象
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
// 在函數的結尾處會將 this 返回,也就是這個新對象
}
構造函數和實例對象的關系
構造函數是根據具體的事物抽象出來的抽象模板
實例對象是根據抽象的構造函數模板得到的具體實例對象
實例對象由構造函數而來,一個構造函數可以生成很多具體的實例對象,而每個實例對象都是獨一無二的;
每個對象都有一個 constructor
屬性,該屬性指向創建該實例的構造函數
反推出來,每一個對象都有其構造函數
console.log(p1.constructor === Person); // => true
console.log(p2.constructor === Person); // => true
console.log(p1.constructor === p2.constructor); // => true
因此,我們可以通過實例對象的 constructor
屬性判斷實例和構造函數之間的關系
注意:這種方式不嚴謹,推薦使用 instanceof
操作符,后面學原型會解釋為什么
console.log(p1 instanceof Person); // => true
console.log(p2 instanceof Person); // => true
constructor 既可以判斷也可以獲取
instanceof 只能用于判斷
2、 構造函數存在的問題
以構造函數為模板,創建對象,對象的屬性和方法都可以在構造函數內部定義;
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();
在該示例中,從表面上看好像沒什么問題,但是實際上這樣做,有一個很大的弊端。
那就是對于每一個實例對象, name
和 say
都是一模一樣的內容,
每一次生成一個實例,都必須為重復的內容,多占用一些內存,如果實例對象很多,會造成極大的內存浪費。
對于這種問題我們可以把需要共享的函數定義到構造函數外部:
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();
這樣確實可以了,但是如果有多個需要共享的函數的話就會造成全局命名空間及變量沖突的問題。
你肯定想到了可以把多個函數放到一個對象中用來避免全局命名空間沖突的問題:
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();
至此,我們利用自己的方式基本上解決了構造函數的內存浪費問題。
但是代碼看起來還是那么的格格不入,那有沒有更好的方式呢?
三、 原型
1、 構造函數的 prototype屬性
JavaScript 的每個對象都繼承另一個父級對象,父級對象稱為 原型 (prototype)對象。
原型也是一個對象,原型對象上的所有屬性和方法,都能被子對象 (派生對象) 共享
通過構造函數生成實例對象時,會自動為實例對象分配原型對象。
而每一個構造函數都有一個prototype屬性,這個屬性就是實例對象的原型對象。
null沒有自己的原型對象。
這也就意味著,我們可以把所有對象實例需要共享的屬性和方法直接定義在構造函數的 prototype
屬性上,
也就是實例對象的原型對象上。
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();
這時所有實例的 name
屬性和 sayhello()
、saycolor
方法,
其實都是同一個內存地址,指向構造函數的 prototype
屬性,因此就提高了運行效率節省了內存空間。
2、 構造函數、實例、原型三者之間的關系
構造函數的prototype屬性,就是由這個構造函數new出來的所有實例對象的 原型對象
前面已經講過,每個對象都有一個 constructor
屬性,該屬性指向創建該實例的構造函數
對象._proto_ (兩邊都是兩個下劃線):獲取對象的原型對象;
console.log(cat1.__proto__ == Cat.prototype); // true
注意:ES6標準規定,_proto_屬性只有瀏覽器才需要部署,其他環境可以不部署,因此不建議使用
3、 原型對象的獲取及修改
上節可以看到,想要獲取一個實例對象的原型對象,有兩種方式:
1:通過實例對象的構造函數的prototype屬性獲取: 實例對象.constructor.prototype
2:通過實例對象的 _proto_ 屬性獲取: 實例對象.__proto__
而這兩種方式,我們都不建議使用:
obj.constructor.prototype
在手動改變原型對象時,可能會失效。
function P() {};
var p1 = new P();
function C() {};
// 修改構造函數的prototype屬性的值為p1
C.prototype = p1; //也就是說,此后所有有C構造函數得到的對象的原型對象都是p1;
var c1 = new C();
console.log(c1.constructor.prototype === p1) // false
推舉設置獲取實例對象的原型的方式:
Object.getPrototypeOf(實例對象) 方法返回一個對象的原型對象。
這是獲取原型對象的標準方法。
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('貓'); //獲取cat1對象的原型對象
var s = Object.getPrototypeOf(cat1);
console.log(s);
Object.setPrototypeOf(實例對象,原型對象) 為現有對象設置原型對象
第一個是實例對象,第二個是要設置成為實例對象的原型對象的對象
這是設置原型對象的標準方法。
function Cat(name) {
this.name = name;
}
var ob = {p:'波斯'};
var cat1 = new Cat('貓');
//設置cat1的原型對象為ob
Object.setPrototypeOf(cat1,ob);
console.log(cat1.p);//cat1的原型對象中有p屬性
console.log(Object.getPrototypeOf(cat1));
console.log(cat1.__proto__);
//注意:如果對象的原型被改變,不會影響構造函數獲取的原型的結果
console.log(Cat.prototype == cat1.__proto__); //false
以上的兩中方法,都是在ES6新標準中添加的;
4、 原型及原型鏈
所有對象都有原型對象;
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('貓');
console.log(cat1.__proto__.__proto__.__proto__);
而原型對象中的屬性和方法,都可以被實例對象直接使用;
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性
- 搜索首先從對象實例本身開始
- 如果在實例中找到了具有給定名字的屬性,則返回該屬性的值
- 如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性
- 如果在原型對象中找到了這個屬性,則返回該屬性的值
- 如果還是找不到,就到原型的原型去找,依次類推。
- 如果直到最頂層的Object.prototype還是找不到,則返回undefined。
而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
對象的屬性和方法,有可能是定義在自身內,也有可能是定義在它的原型對象上。
由于原型本身也是對象,又有自己的原型,所以形成了一條 原型鏈(prototype chain)。
注意,不在要原型上形成多層鏈式查找,非常浪費資源
5、 更簡單的原型語法
我們注意到,前面例子中每添加一個屬性和方法就要敲一遍 構造函數.prototype
。
為減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
}
}
在該示例中,我們將 Person.prototype
重置到了一個新的對象。
這樣做的好處就是為 Person.prototype
添加成員簡單了,但是也會帶來一個問題,那就是原型對象丟失了 constructor
成員(構造函數)。
所以,我們為了保持 constructor
的指向正確,建議的寫法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
// 將這個對象的構造函數指向Person
//constructor: Person, // => 手動將 constructor 指向正確的構造函數
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
}
}
var p = new Person();
6、 原生對象的原型
所有構造函數都有prototype屬性;
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- ……
為內置對象擴展原型方法:
例:
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]
// 其他數組對象也具有相應的方法
var a = [1,2,34,7];
console.log(a.min10()); //[1, 2, 7]
這種技術被稱為猴子補丁,并且會破壞封裝。盡管一些流行的框架(如 Prototype.js)在使用該技術,但仍然沒有足夠好的理由使用附加的非標準方法來混入內置原型。
7、 原型對象的問題及使用建議
性能問題:
在原型鏈上查找屬性時是比較消耗資源的,對性能有副作用,這在性能要求苛刻的情況下很重要。
另外,試圖訪問不存在的屬性時會遍歷整個原型鏈。
//聲明構造函數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 中唯一處理屬性并且不會遍歷原型鏈的方法。
注意:檢查屬性是否undefined
還不夠。該屬性可能存在,但其值恰好設置為undefined
。
//聲明構造函數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、 什么是繼承
- 現實生活中的繼承
- 程序中的繼承
所謂的繼承,其實就是在子類(子對象)能夠使用父類(父對象)中的屬性及方法;
賦予后輩調用祖輩資源的權限,就是繼承;
2、 原型鏈繼承
//聲明構造函數Run
function Run(){
this.p = function(){
console.log(this.name + '跑');
}
}
//聲明構造函數Man
function Man(name){
this.name = name;
}
//設置構造函數Man的原型為Run,實現繼承
Man.prototype = new Run();
var m = new Man('張三');
m.p();
但是,并不建議使用原型鏈繼承,而且JS 中不止有原型鏈繼承,還有其他的繼承方式,后面會講到;
五、 函數進階
1、 函數的聲明及調用
關鍵字聲明
function f1(){
console.log('f1');
}
表達式聲明
var f2 = function(){
console.log('f2');
}
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression)
構造函數方式聲明
var add = new Function(
'x',
'y',
'console.log( x + y )'
);
add(1,2);
上面代碼與下面的代碼等同;
// 等同于
function add(x, y) {
console.log( x + y );
}
add(1,2);
因此,我們只關注前兩種即可,后一種只做了解,不建議使用,這種聲明函數的方式非常不直觀,幾乎無人使用;
那么,關鍵字聲明和表達式聲明 有什么區別?
- 關鍵字聲明必須有名字
- 關鍵字聲明會函數提升,在預解析階段就已創建,聲明前后都可以調用
- 函數表達式類似于變量賦值
- 函數表達式沒有函數名字
- 函數表達式沒有變量提升,在執行階段創建,必須在表達式執行之后才可以調用
// 先調用后聲明
f1();
function f1(){
console.log('f1');
}
//由于“變量提升”,函數f被提升到了代碼頭部,也就是在調用之前已經聲明了
//因此可以正常調用
而表達式方式:
f();
var f = function (){};
// TypeError: undefined is not a function
上面的代碼等同于下面的形式。
var f;
f();
f = function () {};
上面代碼第二行,調用f
的時候,f
只是被聲明了,還沒有被賦值,等于undefined
,所以會報錯。
因此,如果同時采用function
命令和表達式聲明同一個函數,最后總是采用表達式的定義。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
2、 第一等公民
JavaScript 語言將函數看作一種 值,與其它值(數值、字符串、布爾值等等)地位相同。凡是可以使用值的地方,就能使用函數。比如,可以把函數賦值給變量和對象的屬性,也可以當作參數傳入其他函數,或者作為函數的結果返回。函數只是一個可以執行的值,此外并無特殊之處。
由于函數與其他數據類型地位平等,所以在 JavaScript 語言中又稱函數為 第一等公民。
函數作為參數
function eat (callback) {
setTimeout(function () {
console.log('吃完了');
callback()
}, 1000)
}
eat(function () {
console.log('去唱歌');
})
函數作為返回值
function f1(){
var s = 1;
function f2(){
console.log(s);
}
return f2;
}
var f = f1();
f();// 1
JS中一切皆對象,函數也是對象,后面還會講到
3、參數及返回值
(1) 參數
形參和實參
// 函數內部是一個封閉的環境,可以通過參數的方式,把外部的值傳遞給函數內部
// 帶參數的函數聲明
function 函數名(形參1, 形參2, 形參...){
// 函數體
}
// 帶參數的函數調用
函數名(實參1, 實參2, 實參3);
解釋:
- 形式參數:在聲明一個函數的時候,為了函數的功能更加靈活,有些值是固定不了的,對于這些固定不了的值。我們可以給函數設置參數。這個參數沒有具體的值,僅僅起到一個占位置的作用,我們通常稱之為形式參數,也叫形參。
- 實際參數:如果函數在聲明時,設置了形參,那么在函數調用的時候就需要傳入對應的參數,我們把傳入的參數叫做實際參數,也叫實參。
function fn(a, b) {
console.log(a + b);
}
var x = 5, y = 6;
fn(x,y);
//x,y實參,有具體的值。函數執行的時候會把x,y復制一份給函數內部的a和b,函數內部的值是復制的新值,無法修改外部的x,y
var f = function (one) {
console.log(one);
}
f(1, 2, 3)
由于 JavaScript 允許函數有不定數目的參數,所以需要一種機制,可以在函數體內部讀取所有參數。
arguments對象— 函數的實參參數集合
var f = function (one) {
console.log(arguments);
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3);
rest參數
由于JavaScript函數允許接收任意個參數,于是我們就不得不用arguments
來獲取所有參數:
// 只獲取出a,b意外的參數
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);
為了獲取 除了 已定義參數a
、b
之外的參數,我們不得不循環arguments
,并且循環要從索引2
開始以便排除前兩個參數,這種寫法很別扭,只是為了獲得額外的datas
參數,有沒有更好的方法?
ES6標準引入了rest參數,上面的函數可以改寫為:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 結果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 結果:
// a = 1
// b = undefined
// Array []
rest參數只能寫在最后,前面用...
標識,從運行結果可知,傳入的參數先綁定a
、b
,多余的參數以數組形式交給變量rest
,所以,不再需要arguments
我們就獲取了全部參數。
如果傳入的參數連正常定義的參數都沒填滿,也不要緊,rest參數會接收一個空數組(注意不是undefined
)。
注意因為rest參數是ES6新標準,所以,請使用新型瀏覽器;
(2) 返回值
當函數執行完的時候,并不是所有時候都要把結果打印。我們期望函數給我一些反饋(比如計算的結果返回進行后續的運算),這個時候可以讓函數返回一些東西。也就是返回值。函數通過return返回一個返回值
//聲明一個帶返回值的函數
function 函數名(形參1, 形參2, 形參...){
//函數體
return 返回值;
}
//可以通過變量來接收這個返回值
var 變量 = 函數名(實參1, 實參2, 實參3);
函數的調用結果就是返回值,因此我們可以直接對函數調用結果進行操作。
返回值詳解:
如果函數沒有顯示的使用 return語句 ,那么這個函數就沒有任何返回值,僅僅是執行了而已;
如果函數使用 return語句,那么跟在return后面的值,就成了函數的返回值;
如果函數使用 return語句,但是return后面沒有任何值,那么函數的返回值也是:undefined ,
? 函數使用return語句后,這個函數會在執行完 return 語句之后停止并立即退出,也就是說return后面的所有其他代碼都不會再執行。
return后沒有任何內容,可以當做調試來使用;
(3) 遞歸
執行代碼,查看執行結果:
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()
/*
** 執行結果為:
111
222
333
444
fn4
fn3
fn2
fn1
*/
最簡單的一句話介紹遞歸:函數內部自己調用自己
遞歸案例:
計算 1+2+3+......+100 的結果
function sum(n){
if(n > 1){
return n + sum(n - 1);
}else{
return 1;
}
}
var jieguo = sum(5);
console.log(jieguo);
遞歸必須要有判斷條件,不加判斷會死;
4、 作用域
作用域(scope)指的是變量存在的范圍。在 ES5 的規范中,Javascript 只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取;另一種是函數作用域(局部作用域),變量只在函數內部。
函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
var v = 1;
function f() {
console.log(v);
}
f()
// 1
在函數內部定義的變量,外部無法讀取,稱為“局部變量”(local variable)。
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
注意,對于var
命令來說,局部變量只能在函數內部聲明,在其他區塊中聲明,一律都是全局變量。
(1) 作用域鏈
只有函數可以制造作用域結構, 那么只要是代碼,就至少有一個作用域, 即全局作用域。凡是代碼中有函數,那么這個函數就構成另一個作用域。如果函數中還有函數,那么在這個作用域中就又可以誕生一個作用域。
將這樣的所有的作用域列出來,可以有一個結構: 函數內指向函數外的鏈式結構。就稱作作用域鏈。
function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
}
var num = 456;
f1();
5、 函數內部的變量聲明的提升
與全局作用域一樣,函數作用域內部也會產生“變量提升”現象。
var
命令聲明的變量,不管在什么位置,變量聲明都會被提升到函數體的頭部。
function foo() {
console.log(y);//undefined
var y = 'Bob';
}
foo();
注意: JavaScript引擎自動提升了變量y的聲明,但不會提升變量y的賦值。
對于上述foo()函數,JavaScript引擎看到的代碼相當于:
function foo() {
var y; // 提升變量y的申明
var x = 'Hello, ' + y;
alert(x);
y = 'Bob';
}
6、 函數本身的作用域
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
// 討論結果為什么是 1 ?
函數本身也是一個值,也有自己的作用域。
它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關。
總之,函數執行時所在的作用域,是定義時的作用域,而不是調用時所在的作用域。
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) 關于作用域的問題
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
函數內部可以直接讀取全局變量,函數 f1 可以讀取全局變量 n。
但是,在函數外部無法讀取函數內部聲明的變量。
function f1() {
var n = 99;
}
f1()
console.log(n);
有時我們卻需要在函數外部訪問函數內部的變量;
正常情況下,這是辦不到的,只有通過變通方法才能實現。
那就是在函數的內部,再定義一個函數。
function f1() {
var n = 999;
var f2 = function() {
console.log(n);
}
return f2;
}
var f = f1();
f();
上面代碼中,函數f2就在函數f1內部,這時f1內部的所有局部變量,對f2都是可見的。
但是反過來就不行,f2內部的局部變量,對f1就是不可見的。
這就是JavaScript語言特有的”鏈式作用域”結構(chain scope),子級會一層一層地向上尋找所有父級的變量。
所以,父級的所有變量,對子級都是可見的,反之則不成立。
既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內部變量了嗎!
閉包就是函數f2,即能夠讀取其他函數內部變量的函數。
由于在JavaScript語言中,只有函數內部的子函數才能讀取內部變量,
因此可以把閉包簡單理解成“定義在一個函數內部的函數”。
閉包最大的特點,就是它可以“記住”誕生的環境,比如f2記住了它誕生的環境f1,所以從f2可以得到f1的內部變量。
在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁;
閉包(closure)是 Javascript 語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。
理解閉包,首先必須理解變量作用域。
(2)關于JS垃圾回收機制的問題
function f1() {
var n = 99;
console.log(++n);
}
f1(); //100
f1(); //100
當我們在函數內部引入一個變量或函數時,系統都會開辟一塊內存空間;還會將這塊內存的引用計數器進行初始化,初始化值為0如果外部有全局變量或程序引用了這塊空間,則引用計數器會自動進行+1操作當函數執行完畢后,變量計數器重新歸零,系統會運行垃圾回收,將函數運行產生的數據銷毀;如計數器不是 0 ,則不會清楚數據;
這個過程就稱之為 "JS的垃圾回收機制" ;
如果將上節的代碼,改為閉包形式:
function f1() {
var n = 99;
function f2(){
console.log(++n);
}
return f2;
}
var f = f1();
f(); //100
f(); //101
運行代碼發現,函數調用一次,其變量 n 變化一次;
因 函數f1被調用時,返回的結果是f2函數體,也就是說,f2函數被當作值返回給f1的調用者,
但是f2函數并沒有在此時被調用執行,
所以整個 f1 函數體,無法判斷子函數f2會對其產生何種影響,無法判斷 變量n是否會被使用;
即使f1函數被調用結束,整個f1函數始終保留在內存中,不會被垃圾回收機制回收;
閉包的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,
即閉包可以使得它誕生環境一直存在;
注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,所以內存消耗很大。
因此不能濫用閉包,否則會造成網頁的性能問題。
閉包小案例:緩存隨機數
需求:獲取隨機數,隨機數一旦生成,會在程序中多次使用,所以,在多次使用中不變;
function f1(){
var num = parseInt(Math.random() * 100) + 1;
return function (){
console.log(num);
}
}
var f2 = f1();
f2();
f2();
f2();
閉包點贊案例:
<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();
//將 閉包函數 綁定給事件
btnObjs[i].onclick = ff;
}
</script>
思考以下兩段代碼的運行結果:
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
是關鍵,那么this到底是什么?
8、 this 到底是誰
(1) this的不同指向
this
關鍵字是一個非常重要的語法點。毫不夸張地說,不理解它的含義,大部分開發任務都無法完成。
記住一點:this
不管在什么地方使用:它永遠指向一個對象。
下面是一個實際的例子。
var person = {
name: '張三',
describe: function () {
console.log('姓名:' + this.name);
}
};
person.describe()
// "姓名:張三"
上面代碼中,this.name
表示name
屬性所在的那個對象。由于this.name
在describe
方法中調用,而describe
方法所在的當前對象是person
,因此this
指向person
,this.name
就是person.name
。
由于對象的屬性可以賦給另一個對象,所以屬性所在的當前對象是可變的,即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
方法所在的當前對象B
,所以this.name
就指向B.name
。
稍稍重構這個例子,this
的動態指向就能看得更清楚。
function f() {
console.log('姓名:' + this.name);
}
var A = {
name: '張三',
describe: f
};
var B = {
name: '李四',
describe: f
};
A.describe() // "姓名:張三"
B.describe() // "姓名:李四"
上面代碼中,函數f
內部使用了this
關鍵字,隨著f
所在的對象不同,this
的指向也不同。
只要函數被賦給另一個變量,this
的指向就會變。
var A = {
name: '張三',
describe: function () {
console.log('姓名:' + this.name);
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"
上面代碼中,A.describe
被賦值給變量f
,的內部this
就會指向f
運行時所在的對象(本例是頂層對象)。
案例:判斷數值合法性
<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>
上面代碼是一個文本輸入框,每當用戶輸入一個值,鼠標離開焦點就會觸發onChange事件,并執行validate
回調函數,驗證這個值是否在指定范圍。瀏覽器會向回調函數傳入當前對象,因此this
就代表傳入當前對象(即文本框),就然后可以從this.value
上面讀到用戶輸入的值。
JavaScript語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象下運行的,
this
就是函數運行時所在的對象(環境)。這本來并不會讓我們糊涂,但是JavaScript支持運行環境動態切換,也就是說,this
的指向是動態的,沒有辦法事先確定到底指向哪個對象,這才是最初初學者感到困惑的地方。
(2) 使用場合
① 全局環境
全局環境使用this
,它指的就是頂層對象window
。
this === window // true
function f() {
console.log(this === window);
}
f() // true
② 構造函數
構造函數中的this
,指的是實例對象。
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();
③ 對象的方法
方法在哪個對象下,this就指向哪個對象。
var o1 = {
s1:'123',
f1:function (){
console.log(this.s1)
}
}
var o2 = {
s1:'456',
f1:o1.f1
}
o2.f1();
(3) 使用this時的注意事項
① 避免包含多層this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}
f2();
}
}
o.f1()
// Object
// Window
如果要在內層函數中使用外層的this指向,一般的做法是:
var o = {
f1: function () {
console.log(this);
var that = this;
var f2 = function () {
console.log(that);
}
f2();
}
}
o.f1()
// Object
// Object
② 不在循環數組中使用this
var ar = ['a', 'b', 'c'];
ar.forEach(function(v, k, ar){
console.log(this[k])
})
this
的動態切換,固然為JavaScript創造了巨大的靈活性,但也使得編程變得困難和模糊。
有時,需要把this
固定下來,避免出現意想不到的情況;JavaScript提供了call
,apply
,bind
這三個方法,來切換/固定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方法使用的語法規則
函數名稱.call(obj,arg1,arg2...argN);
參數說明:
obj:函數內this要指向的對象,
arg1,arg2...argN :參數列表,參數與參數之間使用一個逗號隔開
apply()方法
函數名稱.apply(obj,[arg1,arg2...,argN])
參數說明:
obj :this要指向的對象
[arg1,arg2...argN] : 參數列表,但是要求格式為數組
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
方法用于將函數體內的this
綁定到具體的某個對象上
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下,"this"指向全局作用域
// 創建一個新函數,將"this"綁定到module對象
// 新手可能會被全局的x變量和module里的屬性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
六、 再談 面向對象
1、 對象
(1) JS的類
在JS中,想要獲取一個對象,有多種方式:
var o1 = {}
var o2 = new Object()
自定義構造函數方式
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());
但是上面這種使用構造函數獲取對象的寫法跟傳統的面向對象語言(比如 C++ 和 Java)差異很大,很容易讓新學習這門語言的程序員感到困惑。
ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。
通過class
關鍵字,可以定義類。
基本上,ES6 的class
可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class
寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。
上面的代碼用 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());
上面代碼定義了一個“類”,可以看到里面有一個constructor
方法,這就是構造方法(后面還會講到),而this
關鍵字則代表實例對象。也就是說,ES5 的構造函數Point
,對應 ES6 的類Point
。
Point
類除了構造方法,還定義了一個toString
方法。注意,定義“類”的方法的時候,前面不需要加上function
這個關鍵字,直接把函數定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。
ES6 的類,完全可以看作構造函數的另一種寫法。
使用的時候,也是直接對類使用new
命令,跟構造函數的用法完全一致。
類同樣也有prototype
屬性,而屬性的值依然是實例對象的原型對象;
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
方法是類的默認方法,通過new
命令生成對象實例時,自動調用該方法。一個類必須有constructor
方法,如果沒有顯式定義,一個空的constructor
方法會被默認添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
類必須使用new
進行實例化調用,否則會報錯。
而類一旦實例化,constructor()
方法就會被執行,就像 人一出生就會哭一樣;
constructor
方法默認返回實例對象(即this
);
但是返回值完全可以指定返回另外一個對象;
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
方法默認返回值盡量不要修改,一旦修改,我們獲取的對象將脫離其原型;
(3) 變量提升
我們知道,在JS中,不管是全局變量還是局部變量都存在變量提升的特性,函數也有提升的特性,也可以先調用后聲明,構造函數也一樣;如下面的代碼,完全沒問題:
var p = new Point();
p.f2();
function Point(){
this.f2 = function(){
console.log('f2');
}
}
但是,需要注意: 類不存在變量提升(hoist),這一點與 ES5 完全不同。
new Man();
class Man{}
// Man is not defined
注意,class只是在原有面向對象的基礎上新加的關鍵字而已,本質上依然沒有改變JS依賴原型的面向對象方式;
2、 再談繼承
(1) 原型鏈繼承的問題
//聲明構造函數Run
function Run(){
this.p = function(){
console.log(this.name + '跑');
}
}
//聲明構造函數Man
function Man(name){
this.name = name;
}
//設置構造函數Man的原型為Run,實現繼承
Man.prototype = new Run();
var m = new Man('張三');
m.p();
// 由構造函數獲取原型
console.log(Man.prototype); // 函數對象 Run
//標準方法獲取對象的原型
console.log(Object.getPrototypeOf(m)); // 函數對象 Run
//獲取對象的構造函數
console.log(m.constructor); // Run 函數
運行上面的代碼,我們發現對象 m
本來是通過構造函數Man
得到的,可是,m
對象丟失了構造函數,并且原型鏈繼承的方式,打破了原型鏈的完整性,不建議使用;
(2) 冒充方式的繼承
前面我們在學習JS面向中的面向對象編程時,談到了繼承;
所謂的繼承,其實就是在子類(子對象)能夠使用父類(父對象)中的屬性及方法;
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();
運行以上代碼可知,由構造函數獲取的對象 b
可以調用函數f1中的屬性及方法;
有運行結果可知,f1
函數中的this
實際指向了對象b
,對象b
實際上已經繼承了f1
這種方式稱為 對象冒充方式繼承,ES3之前的代碼中經常會被使用,但是現在基本不使用了;
為什么不使用了呢?
還是要回到上面的代碼,本質上講,我們只是改變了函數f1
中this的指向,
f1中的this指向誰,誰就會繼承f1;
而call和apply就是專門用來改變 函數中this指向的;
call或apply 實現繼承
function Run(){
this.p = function(){
console.log('ppp');
}
}
function Man(){
//將Run函數內部的this指向Man的實例化對象;
Run.call(this);
}
var m = new Man();
m.p();
//獲取對象的構造函數
console.log(m.constructor); // Man 函數
// 由構造函數獲取原型
console.log(Man.prototype); // 函數對象 Man
//標準方法獲取對象的原型
console.log(Object.getPrototypeOf(m)); // 函數對象 Man
call或apply 實現的繼承依然是使用對象冒充方式實現, 此方式即實現了繼承的功能,同時也不再出現原型鏈繼承中出現的問題;
(3)Object.create() 創建實例對象及原型繼承
構造函數作為模板,可以生成實例對象。但是,有時拿不到構造函數,只能拿到一個現有的對象。我們希望以這個現有的對象作為模板,生成新的實例對象,這時就可以使用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
上面代碼中,對象person1
是person2
的原型,后者繼承了前者所有的屬性和方法;
Object.create
的本質就是創建對象;
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
//下面三種方式生成的新對象是等價的。
如果想要生成一個不繼承任何屬性(比如沒有toString
和valueOf
方法)的對象,可以將Object.create
的參數設為null
。
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
使用
Object.create
方法的時候,必須提供對象原型,即參數不能為空,或者不是對象,否則會報錯。
問題:如果想要實現繼承,應該使用哪種繼承方式?
具體對象實例繼承使用Object.create
構造函數繼承使用對象冒充 call、apply
(4) Class 的繼承
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
}
var m = new Man();
m.p();
Class 可以通過extends
關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
ES5 的繼承,實質是先創造子類的實例對象,然后再將父類的方法添加到上面(Parent.call(this)
)。
ES6 的繼承機制完全不同,是先創造父類的實例對象,然后再用子類的構造函數修改。
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
// 顯式調用構造方法
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
,但是子類并沒有先實例化Run
,導致新建實例時報錯。
class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
constructor(){
// 調用父類實例
super();
}
}
var m = new Man();
m.p();
(5) 繼承總結
原型鏈繼承:
構造函數.prototype --- 會影響所有的實例對象
實例對象._proto_ -------- 只會影響當前對象,需要重新修改constructor屬性的指向,ES6以后只在瀏覽器下部署
Object.setPrototypeOf(子對象,原型) ------ 只會影響當前對象,需要重新修改constructor屬性的指向 (ES6的新語法)
空實例對象 = Object.create(原型) ;創建對象并明確指定原型
call 和apply 本質上是改變函數內部this 的指向,看起來像繼承,但實際上不是繼承
class 的extends 實現繼承,這是標準繼承方式,但是只能在ES6中使用
七、 綜合案例
整體思路:
先玩幾次,思考大概的實現思路;
1:創建基本的靜態頁面;
2:讓div動起來
3:動態創建Div
4:動起來后,填補缺失的div
5:隨機創建黑塊
6:綁定點擊事件
7:點擊判斷輸贏
8:游戲結束后的限制處理
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節點對象
this.main = document.getElementById('main');
this.interval = ''; // 定時器
this.over = false; // 有是否結束的標志
this.sudu = 1; // 初始化下降速度
// 創建DIV的方法
this.cdiv = function(classNames){
// 創建一個div節點對象
var div = document.createElement('div');
// 根據傳入的值,創建不同class屬性的div
if(classNames){
div.className = classNames;
}
return div;
}
//一次生成一行div
this.crow = function(init){
var row = this.cdiv('row');
// 獲取0-3的隨機數
var k = Math.floor(Math.random() * 4)
// 每行div根據隨機數,隨機設置一個黑塊
for(var i = 0; i < 4; i++){
// 隨機出現黑塊
if(i == k){
row.appendChild(this.cdiv('black'));
}else{
row.appendChild(this.cdiv());
}
}
return row;
}
// 初始化運行
this.init = function(){
// 循環創建4行,并添加到main中
for(var i = 0;i < 4; i++){
var row = this.crow();
this.main.appendChild(row);
}
// 綁定點擊事件
this.clicks();
// 設置定時器,使DIV動起來
this.interval = window.setInterval('start.move()' , 15);
}
// 綁定點擊事件
this.clicks = function(){
// 因為在其他作用域中要使用本對象,
// 防止this指向沖突,將this賦值給一個新的變量,在其他作用域中使用新的變量代替this
var that = this;
// 為整個main綁定點擊事件
this.main.onclick = function(ev){
// 通過事件對象,獲取具體點擊的節點
var focus = ev.target;
// 如果游戲已經結束了
if(that.over){
alert('別掙扎了,游戲已經結束了!');
}
// 如果點擊的元素有值為black的class屬性,
// 證明用戶點擊的是黑色塊
else if(focus.className == 'black'){
// 獲取文本框節點對象
var score = document.getElementById('fen');
// 將文本框的值獲取并加1后重新復制
var sc = parseInt(score.value)+1;
score.value = sc;
// 將黑塊變白
focus.className = '';
// 如果此行被點擊過,給這一行發一個'同行證'
focus.parentNode.pass = true;
// 得分每增加5,下降速度提高0.5個像素點
if(sc%5 == 0){
that.sudu += 0.5;
}
}else{
// 點擊的不是黑塊,結束游戲
window.clearInterval(that.interval);
// 游戲已經結束了
that.over = true;
alert('游戲已結束')
}
}
}
// 每調用一次 main 的top值加2像素,main就會向下移動2像素
// 我們只需要不斷調用move,就會讓main不斷下降
this.move = function(){
// 獲取top值
var t = getComputedStyle(this.main, null)['top'];
var tops = parseInt(t);
// 如果tops大于1,證明一行下降結束
if(tops>1){
// 如果此行沒有通行證,游戲結束
if(this.main.lastChild.pass == undefined){
window.clearInterval(this.interval);
// 游戲已經結束了
this.over = true;
alert('游戲已結束')
}else{ // 如果有通行證
// 如果大于5行,刪除最后一行
if(this.main.children.length >= 5) {
this.main.removeChild(this.main.lastChild);
}
}
// 下降結束一行,則在最頂部增加一行,完成下降的連續性
var row = this.crow();
this.main.insertBefore(row,this.main.firstChild);
// 并重新隱藏新加的一行
this.main.style.top = '-100px';
}else{
// 定時器每調用一次,top 值修改一次
// 完成下降動作
this.main.style.top = tops + this.sudu +'px';
}
}
}
var start = new Youxi();
start.init();
</script>
</html>