JS高級3-語言特性

一、 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 個步驟:

  1. 創建一個空對象,作為將要返回的對象實例。
  2. 將這個空對象的原型,指向構造函數的prototype屬性。先記住,后面講
  3. 將這個空對象賦值給函數內部的this關鍵字。
  4. 執行構造函數內部的代碼。
  5. 返回新對象
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();

在該示例中,從表面上看好像沒什么問題,但是實際上這樣做,有一個很大的弊端。
那就是對于每一個實例對象, namesay 都是一模一樣的內容,
每一次生成一個實例,都必須為重復的內容,多占用一些內存,如果實例對象很多,會造成極大的內存浪費。

對于這種問題我們可以把需要共享的函數定義到構造函數外部:

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

解釋:

  1. 形式參數:在聲明一個函數的時候,為了函數的功能更加靈活,有些值是固定不了的,對于這些固定不了的值。我們可以給函數設置參數。這個參數沒有具體的值,僅僅起到一個占位置的作用,我們通常稱之為形式參數,也叫形參。
  2. 實際參數:如果函數在聲明時,設置了形參,那么在函數調用的時候就需要傳入對應的參數,我們把傳入的參數叫做實際參數,也叫實參。
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);

為了獲取 除了 已定義參數ab之外的參數,我們不得不循環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參數只能寫在最后,前面用...標識,從運行結果可知,傳入的參數先綁定ab,多余的參數以數組形式交給變量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.namedescribe方法中調用,而describe方法所在的當前對象是person,因此this指向personthis.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提供了callapplybind這三個方法,來切換/固定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

上面代碼中,對象person1person2的原型,后者繼承了前者所有的屬性和方法;

Object.create 的本質就是創建對象;

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();

//下面三種方式生成的新對象是等價的。

如果想要生成一個不繼承任何屬性(比如沒有toStringvalueOf方法)的對象,可以將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>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容

  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,146評論 0 21
  • ??面向對象(Object-Oriented,OO)的語言有一個標志,那就是它們都有類的概念,而通過類可以創建任意...
    霜天曉閱讀 2,131評論 0 6
  • 面向對象的語言有一個標志,那就是它們都有類的概念,而通過類可以創建任意多個具有相同屬性和方法的對象。ECMAScr...
    DHFE閱讀 988評論 0 4
  • 假如你想要一件東西 就放它走 它若能回來找你 就永遠屬于你 它若不回來 那根本就不是你的 無論心情怎樣,都要面帶微...
    二次城市拾憶者閱讀 276評論 0 2
  • 今晚刷著朋友圈,看到了一個大我一屆的初中師兄P他發了一個小視頻,點開一看,視頻的背景是一個KTV,一男一女,動作親...
    林小鹿閱讀 221評論 0 0