# JS-3 ## 複習 > - JS 特色 > - 編程語言 > - 解釋執行: > - 編譯一行, 執行一行, > - 慢 > - 靈活, 動態 > - vs. 編譯執行( C, java ): > - 編譯全部, 依序執行, > - 快 > - 頭等函數( first-class function ) > - vs. C, java : 頭等類 > - 執行環境: 宿主( host environment ) > - JS 組成 : ECMAScript, DOM, BOM > - JS 執行過程: > - hoisting 全局(變量, 函數) -> 執行全局 code > - 執行全局 code 中遇到執行函數的code : > - hoisting 函數(變量,形參, 函數) -> 執行函數 > ## 瀏覽器 ![](https://i.imgur.com/mHXRgvO.jpg) > - 瀏覽器一開始會去找DNS要域名的IP, 然後才去該服務器要東西 ![](https://i.imgur.com/15o5EnQ.png) > - User Interface : 就是看得到的介面 > - Browser engine : 查詢或操作 Rendering engine > - ==Rendering engine== : 解析HTML, CSS, 並將其渲染成一個 DOM tree, 存放於內存中 > - NetWorking : > - 接收回應, 交給 Rendering engine 處理 > - 發送請求給 server > - ==JS Interpreter== : > - 執行 JS Code, 實際上就是操作 DOM tree, > - 操作完成後 Rendering engine 會更新 DOM tree, > - 更新完後經過 Browser engine 顯示到 User Interface > - UI Backend : > - 繪製 alert 等窗口 > - 直接與 OS 交互, 調用 OS 的 API > - Data Persistence : 存儲一些數據 sessionStorage > ## 物件導向程式設計(object oriented programming, oop) > - 又稱 面向對象編程 ( 中國 ) ### Object > - 一個事物的抽象 > - 一個含有 property 和 method 的容器 > - 人是一個類別, 我就是一個對象, 我的名字就是 property, 我會睡覺就是 method ### object oriented > - 把相關的物件封裝在一個對象中: 一包一包的對象 > - 把問題拆成一個一個對象, 每個對象負責他們的功能 > - vs. procedural programming > - 程序式程式設計: 一條一條的指令 > - 分析問題, 列出解決步驟, 一步步實現, 一個一個調用 > - procedural programming 就像一盤炒飯, 把一堆東西丟進去炒 > - object oriented 就像咖哩飯, 拿到飯淋上咖哩就成了咖哩飯 > - object oriented 特性 > - 封裝 ( Encapsulation ) > - 繼承 ( Inheritance ) > - 多型 ( Polymorphis ) ```javascript= /* procedural programming var apple = {price: 30, sweet: 5}; var banana = {price: 20, sweet: 3}; console.log(apple.price, apple.sweet); console.log(banana.price, banana.price); */ // object oriented programming function Fruit(name, price, sweet) { this.name = name; this.price = price; this.sweet = sweet; this.print = function () { console.log(this.name, this,price, this.sweet); } } var apple = new Fruit('apple', 30, 5); var banana = new Fruit('banana', 20, 3); apple.print(); banana.print(); ``` > - 優點: > - 分工明確: 每個人負責不能的對象物件或調用 > - 容易維護: 哪個對象出錯就找哪個 > - 物件可重複使用 > - 高度模塊化 > - object oriented programming 就是 procedural programming 的封裝 > - JS 的看起來就跟 class 一樣 > - create class -> Instance -> Instance() ### 創建對象 > #### 簡單創建(靜態對象) > - 以下兩種差不多意思, 問題也差不多 > - 當要創建多個類似對象時, 重複的 code 會很多 > - `new object()` ```javascript= var tmp = new Object(); tmp.name = 'GodJJ'; tmp.age = 20; tmp.print = function () { console.log(this.name + ' ' + this.age + ' 歲') } tmp.print(); ``` > - `{}` ```javascript= var tmp = { name: 'GodJJ', age: 20, print: function () { console.log(this.name + ' ' + this.age + ' 歲') }, } tmp.print(); ``` > #### 工廠函數 > - 基本上該有的功能封裝都可以裝起來了 > - 小缺點: 自定義對象返回都是 object 類型, 沒有其他類 > - 因為都是自己在函數裡面用`new Object` 或 `{}` 創建對象後, 返回回來 > - `instanceof` : 判斷對象是否是某個實例 ```javascript= // 函數 function createLiver(name, age) { var o = {} o.name = name; o.age = age; o.print = function () { console.log(name + ' ' + age + ' 歲'); } return o } // 調用函數, 返回一個 {} var GodJJ = createLiver('Godjj', 20); var Toyz = createLiver('Toyz', 20); // 調用 {} 裡面某個屬性指向的的函數 GodJJ.print(); Toyz.print(); // 小缺點: // 構造函數就可以用自己定義的類來創建 var a = []; // new Array() console.log(a instanceof Object); // true console.log(a instanceof Array); // true // 函數無法自己定義自己的類, 自定義的對象都是用 Object 創建 console.log(GodJJ instanceof Object); // true // console.log(GodJJ instanceof ?); ? 沒東西可以打 ``` > #### 構造函數 > - new 做的事情: > - 在內存創建一個空對象 > - 設置this, 指向創建的對象 > - 執行構造函數的code > - 返回對象 ```javascript= // 函數 function Liver(name, age) { this.name = name; this.age = age; this.print = function () { console.log(this.name + ' ' + this.age + ' 歲'); } } // 創建實例對象 var GodJJ = new Liver('GodJJ', 20); var Toyz = new Liver('Toyz', 20); // 調用實例對象中的方法 GodJJ.print(); Toyz.print(); // 構造函數就可以解決工廠函數的問題 console.log(GodJJ instanceof Liver); // true console.log(GodJJ instanceof Object); // ture var a = []; console.log(a instanceof Liver); // false // 補充: // constructor 屬性: 返回構造函數 console.log(GodJJ.constructor); // ƒ Liver(name, age) { [...] } console.log(a.constructor); // ƒ Array() { [native code] } ``` > #### 靜態對象 vs 實例對象 > - 實例對象只能訪問實例屬性跟方法, 不能訪問靜態 > - 構造函數也只能訪問靜態, 不能訪問實例 ```javascript= // 靜態對象 var Math = { PI: 3.14, // 靜態屬性 Max: function () {console.log(100)}, // 靜態方法 Min: function () {}, } // 調用靜態方法 Math.Max(); // ------------------------------------- // // 構造函數 function Liver(name, age) { this.name = name; // 實例屬性 this.age = age; this.print = function () { // 實例方法 console.log(this.name + ' ' + this.age + ' 歲'); } } // 實例對象 var GodJJ = new Liver('GodJJ', 20); // 調用實例方法 GodJJ.print(); // ---------------------------------- // // 添加『靜態』屬性 // 靜態屬性在構造函數本身添加 Liver.apple = 20; // 添加實例屬性在實例對象添加 GodJJ.banana = 30; // 實例對象只能訪問實例屬性跟方法, 不能訪問靜態 // 構造函數也只能訪問靜態, 不能訪問實例 console.log(Liver.apple); // 20 console.log(GodJJ.banana); // 30 console.log(Liver.banana); // undefined console.log(GodJJ.apple); // undefined // 即使從新實例也是 GodJJ = new Liver('GodJJ', 20); console.log(GodJJ.apple); ``` ### 構造函數的問題 > - 複雜數據類型會新開闢空間, 即使功能相同 > - 如此會造成空間浪費 ```javascript= function Liver(name, age) { this.name = name; this.age = age; this.print = function () { console.log(this.name + ' ' + this.age + ' 歲'); } } var GodJJ = new Liver('GodJJ', 20); var Toyz = new Liver('Toyz', 20); GodJJ.print(); Toyz.print(); console.log(GodJJ.print === Toyz.print); // false // 兩個實例對象都會各自創建 print 方法, 並將 print 指向各自創建的 function 中 // 但這明明是一樣的功能, 浪費空間與效能 ``` > - 解法一: 把 function 拉到外面創建一個後, 讓構造函數直接指向 > - 缺點: 為了要讓類屬性指向, 拉出來的函數要有名字, 取名真的痛苦 ```javascript= function Liver(name, age) { this.name = name; this.age = age; this.print = print; // 指向函數 } // 拉到外面創建一個函數 function print() { console.log(this.name + ' ' + this.age + ' 歲'); } var GodJJ = new Liver('GodJJ', 20); var Toyz = new Liver('Toyz', 20); GodJJ.print(); Toyz.print(); console.log(GodJJ.print === Toyz.print); // true // 如此兩個實例對象的 print 屬性都指向同一個 function 空間了 ``` > - 解法二: `prototype`( 原型 / 原型對象 ) > - 每個構造函數都有一個 prototype 屬性 > - `prototype` 是一個對象 > - 把 function 塞進 prototype 對象裡 > - 小結: 一般情況下, > - 公共屬性放構造函數裡 (簡單數據類型) > - 公共方法放 `prototype` 裡 (複雜數據類型) ```javascript= function Liver(name, age) { this.name = name; this.age = age; } // console.log(Liver.prototype); // prototype 是一個對象 Liver.prototype.print = function () { // 不用取名真開心 console.log(this.name + ' ' + this.age + ' 歲'); } var GodJJ = new Liver('GodJJ', 20); var Toyz = new Liver('Toyz', 20); GodJJ.print(); Toyz.print(); console.log(GodJJ.print === Toyz.print); // true ``` ```python= # 突然想寫一下 python # class class Liver(object): def __init__(self, name, age): self.name = name # 實例屬性 self.age = age def printLiver(self): # 實例方法 print('%s %s 歲'%(self.name, self.age)) # 實例對象 godjj = Liver('godjj', 20) toyz = Liver('toyz', 20) # 調用實例方法 godjj.printLiver() # godjj 20 歲 toyz.printLiver() # toyz 20 歲 if True: print(id(toyz.printLiver), id(godjj.printLiver)) # 4494304584 4494304584 # python 沒有這個困擾, 兩個都是一樣的 ``` ### prototype 與 constructor > - 任何函數都有一個 `prototype`, 而預設的 `prototype` 都有一個 `constructor` > - 構造函數的實例對象都有一個__proto__ > - 對象中的 `__proto__` 其實就是引用 構造函數的`prototype` > - `console.log(godjj.__proto__ === Liver.prototype); // true` > - 調用屬性或方法時, > - 先從對象本身中找, > - 後去對象的 \_\_proto__ 找, 找不到再繼續往上, 一直找到 Object 的 prototype > - 都沒有就報錯 > - `constructor` 在構造函數中的`prototype`引用構造函數 > - `console.log(Liver.prototype.constructor === Liver) // true` > - 關係圖 > <img src='https://i.imgur.com/7aZ1okT.jpg' style='width: 300px'/> > ```javascript= function Liver(name, age) { this.name = name; this.age = age; this.print = function () { console.log(123); } } Liver.prototype.print = function () { console.log(this.name + ' ' + this.age + ' 歲'); } var godjj = new Liver('GodJJ', 20); godjj.print(); // 123 console.dir(godjj); // Liver // age: 20 // name: "GodJJ" // print: ƒ () // __proto__: Object -> 引用構造函數的 prototype // print: ƒ () // constructor: ƒ Liver(name, age) -> 引用構造函數 // __proto__: Object -> 註 // 實例屬性的 __proto__ 引用 構造函數的 prototype 的址 console.log(godjj.__proto__ === Liver.prototype); // true // constructor 引用構造函數的址 console.log(godjj.constructor); // ƒ Liver(name, age) {[...]} console.log(godjj.constructor === Liver); // true // 註: // 構造函數也是 他的構造函數 的實例, // 所以構造函數的 __proto__ 也引用的他的構造函數的 prototype // Python: type -> class -> 實例對象 的概念 // 整理: console.dir(Liver); // 構造函數 console.dir(godjj); // 實例對象 console.log(godjj.__proto__.constructor); // ƒ Liver(name, age) {[...]} // constructor 指向構造函數 // 實例對象(構造函數)的原型對象 // 兩個指向同一個址 console.dir(godjj.__proto__); console.dir(Liver.prototype); console.log(godjj.__proto__ === Liver.prototype); // true // 構造函數的上面是 Object console.dir(godjj.__proto__.__proto__); // Object 的 prototype console.log(godjj.__proto__.__proto__.constructor); // ƒ Object() { [native code] } console.log(Object.prototype === godjj.__proto__.__proto__); // true // Object的上面是 null console.dir(godjj.__proto__.__proto__.__proto__); // null ``` > - 這也可以解釋為什麼每個函數都有一些方法, 例如 toString() > - 因為 toString 在 Object 的 prototype 裡 > - 即使所有實例都沒有寫 toString, 最後還是會在 Object.prototype 找到來調用 ```javascript= // 呈上函數 console.log(godjj.toString) // "[object Object]" ``` > - 屬性查找與設置 > - 承前, 查找是先找對象本身, 找不到就往上 \_\_proto__ 找到 Object.prototype 為止 > - 惟, 設置不會往上找, 如果對象本身沒有, 他就直接新增一個 ```javascript= function Liver(name, age) { this.name = name; this.age = age; } Liver.prototype.sex = '男'; var godjj = new Liver('GodJJ', 20); var toyz = new Liver('toyz', 20); console.log(godjj.sex); // 男 godjj.sex = '女'; console.log(godjj.sex); // 女 console.log(toyz.sex); // 男 // 證明設置時沒有到 __proto__ 找到 sex 來改, 否則 toyz 的 sex 應該是女 console.dir(godjj); // Liver // age: 20 // name: "GodJJ" // sex: "女" <- 設置時直接塞進對象本身, 利用查找優先的特性來輸出剛設置的東西 // __proto__: Object // sex: "男" // constructor: ƒ Liver(name, age) // __proto__: Object console.dir(toyz); // Liver // age: 20 // name: "toyz" // __proto__: Object // sex: "男" // constructor: ƒ Liver(name, age) // __proto__: Object ``` > #### 重新設置 `prototype` > - 如果有多個東西想塞進 prototype, 原本的形式太麻煩了 > - 此時就可以考慮直接修改 prototype 的指向 > - 但修改後所指向的對象要記得把 constructor 指回原構造函數 > - 另外重新指向不是動態添加, 所以修改前所創建的實例會沒辦法用修改後的內容 ```javascript= function Liver(name, age) { this.name = name; this.age = age; } /* 太囉唆 Liver.prototype.haha = function () { console.log('haha'); } Liver.prototype.wowo = function () { console.log('wowo'); } */ // 直接修改 prototype 對象 Liver.prototype = { haha: function () { console.log('haha'); }, wowo: function () { console.log('wowo'); } } var godjj = new Liver('godjj', 20); godjj.haha(); // haha console.log(godjj.constructor); // ƒ Object() { [native code] } console.dir(godjj); // Liver // age: 20 // name: "godjj" // __proto__: -> Liver.prorotype // haha: ƒ () // wowo: ƒ () // __proto__: -> Object.prototype // constructor: ƒ Object() // 由於設置 prototype 時沒有設置 constructor, 導致 prototype 指向的對象沒有 // 然後就繼續往上找, 所以輸出的結果是 Object ``` > - 解決方法就是修改 prototype 指向時, 要記得把手動設置 constructor 並指回去 ```javascript= function Liver(name, age) { this.name = name; this.age = age; } Liver.prototype = { constructor: Liver, haha: function () { console.log('haha'); }, } var godjj = new Liver('godjj', 20); console.log(godjj.constructor); // Liver(name, age) { [...] } ``` > - 動態添加 與 重新指向 ```javascript= // 動態添加 function Liver(name, age) { this.name = name; this.age = age; } var godjj = new Liver('godjj', 20); Liver.prototype.haha = function () { console.log('haha'); } godjj.haha(); // haha ``` ```javascript= // 重新指向 function Liver(name, age) { this.name = name; this.age = age; } /* 報錯 var godjj = new Liver('godjj', 20); Liver.prototype = { constructor: Liver, haha: function () { console.log('haha'); }, } godjj.haha(); // Error: godjj.haha is not a function */ Liver.prototype = { constructor: Liver, haha: function () { console.log('haha'); }, } var godjj = new Liver('godjj', 20); // 重新指向完再用 godjj.haha(); // haha ``` ### prototype 應用 > - 為 Array, String 等新增方法 ```javascript= // 為 Array 新增偶數和的方法 Array.prototype.getEvenSum = function () { var sum = 0; for (var i = 0; i < this.length; i++) { if (this[i] % 2 === 0) { sum += this[i] } } console.log(sum); } var arr = [1,3,4,6]; arr.getEvenSum(); ``` > - Array, String 等的prototype 是不可以重新指向的, 只能動態添加 ```javascript= console.dir(Array.prototype); Array.prototype = { constructor: Array, } console.dir(Array.prototype); // 沒變 ``` ### 實作: 隨機方塊 <img src="https://i.imgur.com/2gTn5dE.png" style="width: 200px"/> `$ vi index.html` ```htmlmixed= <head> <meta charset='utf-8'> <title></title> <link rel='stylesheet' href='css/style.css' /> </head> <body> <div class='container'></div> <script src='js/tools.js'></script> <script src='js/box.js'></script> <script src='js/main.js'></script> </body> ``` `vi css/style.css` ```css .container { height: 600px; width: 600px; background: darkgray; position: relative; } ``` `$ vi js/tools.js` ```javascript= var Tools = { getRandom: function (min, max) { return Math.floor(Math.random() * (max-min+1) + min) }, } ``` `vi js/box.js` ```javascript= function createBox(parent, option) { option = option || {}; this.height = option.height || 30; this.width = option.height || 30; this.backgroundColor = option.backgroundColor || 'orange'; this.top = option.top || 0; this.left = option.left || 0; this.div = document.createElement('div'); parent.appendChild(this.div); this.parent = parent this.init(); } createBox.prototype.init = function () { this.div.style.height = this.height + 'px'; this.div.style.width = this.width + 'px'; this.div.style.backgroundColor = this.backgroundColor; this.div.style.top = this.top + 'px'; this.div.style.left = this.left + 'px'; this.div.style.position = 'absolute'; } createBox.prototype.randomPosition = function () { // 寫法一: 讓盒子不超出框框的前提下, 自由串動 // var x = Tools.getRandom(0, this.parent.offsetWidth - this.div.offsetWidth); // var y = Tools.getRandom(0, this.parent.offsetHeight - this.div.offsetHeight); // 寫法二: 框框分隔很多格, 讓盒子移動到各個格子內 var x = Tools.getRandom(0, this.parent.offsetWidth / this.div.offsetWidth - 1) * this.div.offsetWidth; var y = Tools.getRandom(0, this.parent.offsetHeight / this.div.offsetHeight - 1) * this.div.offsetHeight; // console.log(x,y); this.div.style.left = x + 'px'; this.div.style.top = y + 'px'; } createBox.prototype.randomColor = function () { var r = Tools.getRandom(0, 255); var g = Tools.getRandom(0, 255); var b = Tools.getRandom(0, 255); console.log(r,g,b); this.div.style.backgroundColor = 'rgb(' + r + ',' + g + ',' + b + ')'; } ``` `$ vi js/main.js` ```javascript= var container = document.getElementsByClassName('container')[0]; var r,g,b,box, arr=[]; for (var i = 0; i < 10; i++) { r = Tools.getRandom(0, 255); g = Tools.getRandom(0, 255); b = Tools.getRandom(0, 255); box = new createBox(container, { backgroundColor: 'rgb(' + r + ',' + g + ',' + b + ')', }) box.randomPosition(); arr.push(box); } setInterval(function () { // 原本我用 container.children來找, // 可是這找出來的對象DOM的, 跟 createBox 不同 // 導致這個對象是沒有 createBox 塞的那些方法 // 所以我就把 createBox 實例塞進陣列來遍歷 for (var i = 0; i < arr.length; i++) { arr[i].randomPosition(); arr[i].randomColor(); } }, 1000); // console.log(container.children[0].constructor); // ƒ HTMLDivElement() { [native code] } // console.log(arr[0].constructor); // ƒ createBox(parent, option) { [...] } ``` ### 自調用函數 > - 由於js直接命名時都是在全局作用域寫 > - 為了避免命名衝突以及各種資料保密等目的, 必須將所有個code包成一個局部作用域 > 否則會出現以下情形 `$ vi a.js` ```javascript= var a = 100 ``` `$ vi b.js` ```javascript= var a = 200 ``` `$ test.html` ```htmlmixed= <body> <script src='a.js'></script> <script src='b.js'></script> </body> ``` ![](https://i.imgur.com/Pzf51xT.png) > - 首先, 每個js檔可能是不同人做的, 難免會取道相同的名字, 導致資料被取代 > - 另外, 一般人怎麼可能可以直接在F12中看你的資料 > - 為了解決此問題, 應該將所以 code 包起來 > - 不過沒有完全解決命名衝突問題, 只能部分解決 `$ vi a.js` ```javascript= (function () { var a = 100 window.a = a; // 如果沒有給出去, 用戶就永遠訪問不到這個變量 // 如此就能控制訪問權限 })() ``` `$ vi b.js` ```javascript= (function () { var a = 200; window.a = a; // 不過給出去的變量名如果還是一樣, 那還是沒解決命名衝突 // 不過解決了沒給出去的變量干擾其他人的變量問題 })() ``` ### 自調用函數的問題 ```javascript= (function () { console.log(1); })() (function () { console.log(2); })() /* 運行結果: 1 Uncaught TypeError: (intermediate value)(...) is not a function */ /* 原因: 執行完第一個函數時, 會有一個返回值 undefined 接著繼續往下執行, 中間沒有任何結束, 對電腦來說要執行的東西變成 undefined(function () {console.log(2);})() 也就是報錯訊息說的 (intermediate value)(...) 不是一個 function */ /* 解決辦法就是加 ; 在自調用函數之間 */ (function () { console.log(1); })(); (function () { console.log(2); })(); // 輸出結果: 1 2 /* 但在後面加可能會有另外一個問題 */ var fn = function () { console.log(1); } (function () { console.log(2); })(); /* 輸出結果: 1 Uncaught TypeError: (intermediate value)(...) is not a function */ /* 跟一開始一模一樣, 原因也是一樣的 上面定義函數後面沒有結束, 對電腦來說整個語句變成 var fn = function () { console.log(1); }(function () { console.log(2); })(); 又, 不是每個人都會習慣在每句code的結尾都加 ; 所以規範上與實際看到別人寫的 code 的自調用都是在前面加 ; 來結束前面的語句 */ // 實際的自調用寫法: ;(function () { console.log(1); })() ;(function () { console.log(2); })() ``` ### 簡易貪吃蛇邏輯 > - 首先, 先判斷需要什麼對象: 蛇, 食物, 遊戲控制器 > - 接著陸續創建對象, 太多了我也不知道怎麼寫 `$ vi index.html` ```htmlmixed= <head> <meta charset='utf-8'> <title></title> <link href='css/style.css' rel='stylesheet'/> </head> <body> <div class='gameBox'></div> <script src='js/tools.js'></script> <script src='js/candy.js'></script> <script src='js/snake.js'></script> <script src='js/game.js'></script> <script src='js/main.js'></script> </body> ``` `$ vi css/style.css` ```css= .gameBox { height: 600px; width: 600px; background: gray; position: relative; } ``` `$ vi js/tools.js` ```javascript= ;(function () { var Tools = { getRandom: function (min, max) { return Math.floor(Math.random() * (max-min+1) + min) }, } window.Tools = Tools; })() ``` `$ vi js/candy.js` ```javascript= ;(function () { var elem = []; // 存食物資料(位置) // 設置食物初始值 function Candy(options) { // 避免如果調用時沒有傳參, 導致options = undefined // 而造成 undefined.x 等的報錯 options = options || {}; this.backgroundColor = options.backgroundColor || 'orange'; this.x = options.x || 0; this.y = options.y || 0; this.height = options.height || 30; this.width = options.width || 30; } // 顯示食物 Candy.prototype.render = function (parent) { // 生成下一個食物前, 需要把上一個食物砍了, 否則畫面上會有多個食物 // 玩家不需要知道你怎麼刪的, 所以不需要把刪除方法放在Candy裡面 // 因為 Candy之後會傳出去, 玩家不會去調用刪除, 否則就沒東西吃了 // 這裡只需要調用刪除即可 remove(); // 生成食物 var div = document.createElement('div'); this.x = Tools.getRandom(0, parent.offsetWidth / this.width - 1) * this.width; this.y = Tools.getRandom(0, parent.offsetHeight / this.height - 1) * this.height; div.style.top = this.y + 'px'; div.style.left = this.x + 'px'; div.style.height = this.height + 'px'; div.style.width = this.width + 'px'; div.style.backgroundColor = this.backgroundColor; div.style.position = 'absolute'; parent.appendChild(div); // 放到畫面上 elem.push(div); // 把食物記錄起來 } // 由於一般人不會自己去『調用』刪除, 否則貪吃蛇就沒食物吃了 // 所以不用寫在Candy構造函數中 // 一般來說貪吃蛇的食物會再被吃掉的時候生成新食物, // 也就是在生成之前調用刪除函數即可 function remove() { // 循環刪除陣列資料時, 最好從後向前刪, 否則會有漏刪的麻煩 // 因為前面刪掉後, 資料位置會往前補 // arr = [0,1,2] // i = 0 -> 把 [0] 刪掉, 此時 arr = [1,2] // i = 1 -> 把 [1] 刪掉, 此時 arr=[1] -> 漏刪 for (var i = elem.length - 1; i >= 0; i--) { elem[i].parentNode.removeChild(elem[i]); elem.splice(i,1); } } window.Candy = Candy; // 如果我沒給出去, 使用者就訪問不到 // 像 remove 跟 elem 就訪問不到 })() ``` `$ vi js/snake.js ` ```javascript= ;(function () { var elem = []; // 設置蛇的基本資料 function Snake(options) { options = options || {}; this.height = options.height || 30; this.width = options.width || 30; this.direction = options.direction || 'right'; // 蛇的身體有很幾格, 所以用陣列存放 this.body = [ {x:3, y:2, color: 'red'}, // 第一格是蛇頭 {x:2, y:2, color: 'skyblue'}, // 預設兩格身體, x,y 是格數 {x:1, y:2, color: 'skyblue'}, ] } // 顯示蛇 Snake.prototype.render = function (parent) { // 移動蛇時要把原本的資料刪掉, 否則會有一堆身體跟頭 // 跟食物一樣, 不用寫給玩家知道怎麼刪的, 也不用讓他們自己刪 remove(); var obj, div; // 避免在循環裡定義, 這樣會一直開記憶體空間 // 先定義 len 是因為避免每次 if 的時候都要去找一次 this.body.length 的值 // 先把固定值拿出來, 效率比較高 for (var i=0, len=this.body.length; i < len; i++) { obj = this.body[i]; div = document.createElement('div'); div.style.left = obj.x * this.width + 'px'; div.style.top = obj.y * this.height + 'px'; div.style.backgroundColor = obj.color; div.style.position = 'absolute'; div.style.height = this.height + 'px'; div.style.width = this.width + 'px'; parent.appendChild(div); // 創建完要把資料存起來 elem.push(div); } } // 蛇的移動 // 蛇在移動時, 給格身體的下一步應該是前一格的資料 // 蛇頭的移動方式應該是看方向來決定 Snake.prototype.move = function (candy, parent) { var obj = this.body // 蛇身體移動移動, 就是前一格的 xy for (var i = obj.length - 1; i > 0; i--) { obj[i].x = obj[i-1].x; obj[i].y = obj[i-1].y; } // 蛇頭的移動視方向而定 switch (this.direction) { case 'right': obj[0].x += 1; break; case 'left': obj[0].x -= 1; break; case 'top': obj[0].y -= 1; break; case 'bottom': obj[0].y += 1; break; } // 判斷蛇有沒吃到食物, 吃到就生成食物與蛇變長 // 蛇移動的過程才會吃到食物, // 所以在這裡判斷是否吃到食物 var headX = obj[0].x * this.width; var headY = obj[0].y * this.height; // 判斷吃到了沒 // 要先拿到食物對象來判斷, 所以要把食物傳進來 if (candy.x == headX && candy.y === headY) { // 多一格身體 // 吃到食物時, 會增長蛇的長度一格 // 長大後原本最後一格會往前移(原本的移動), 而最後一格的位置就是生出來的那格 // 所以把最後一格的資料拿出來後, 添加一個對象到this.body裡, 把資料抄過去 var lastBox = obj[obj.length-1]; // 先取末格資料 obj.push({ // 身體的資料多一筆, 把末格資料塞進去 x: lastBox.x, y: lastBox.y, color: lastBox.color, }) // 生成食物 // 需要把生成的食物添加到遊戲盒子中, // Snake對象沒有存起來, 所以要馬上面要存起來, 要馬傳參進來 candy.render(parent); } } function remove() { for (var i = elem.length -1; i >= 0; i--) { elem[i].parentNode.removeChild(elem[i]); elem.splice(i, 1); } } window.Snake = Snake; })() ``` `$ vi js/game.js` ```javascript= // 遊戲控制器 ;(function () { var that; // 遊戲所需的材料都拿到 function Game(parent) { this.candy = new Candy(); this.snake = new Snake(); this.parent = parent; that = this; } // 遊戲開始 Game.prototype.start = function () { // 初始畫面開啟 this.candy.render(this.parent); this.snake.render(this.parent); // 開始跑 // 貪吃蛇遊戲不外乎就讓蛇跑與控制方向鍵 run(); // 能解藕就盡量解 keyDirection(); } // 讓蛇跑 function run() { var headX, headY, maxX, maxY; var timer = setInterval(function () { // this.snake.move() // this.snake 在這裡是訪問不到的, // 因為調用 setInterval 的是 window, // 也就是 this 指向的是 window, // 為了訪問到Game, 所以創了一個全局變量 that 存放 Game 對象 // 此時就可以透過 that 來找到 Game // 讓蛇跑就是每次移動後顯示 that.snake.move(that.candy, that.parent); that.snake.render(that.parent); // GameOver的規則: 撞牆 // 舌頭的位置 headX = that.snake.body[0].x; headY = that.snake.body[0].y; // 牆的右跟下邊界 maxX = that.parent.offsetWidth / that.snake.width - 1; // 從0開始, 所以-1 maxY = that.parent.offsetHeight / that.snake.height -1; // 超出去就停 // 這裡判斷不太精確, 之後有空可以再改良 if (headX < 0 || headX > maxX) { console.log(1); clearInterval(timer); } if (headY <0 || headY > maxY) { console.log(1); clearInterval(timer); } }, 100) } // 獲取方向鍵 function keyDirection() { document.onkeydown = function (e) { // console.log(e.keyCode); // 先測試一下上下左右是多少 switch (e.keyCode) { case 38: that.snake.direction = 'top'; break; case 40: that.snake.direction = 'bottom'; break; case 37: that.snake.direction = 'left'; break; case 39: that.snake.direction = 'right'; break; } } } window.Game = Game; })() ``` `$ vi js/main.js ` ```javascript= // 遊戲執行 ;(function () { var gameBox = document.getElementsByClassName('gameBox')[0]; var game = new Game(gameBox); game.start(); })() ``` ### 輸出優化 > - 輸出時必須減少使用者加載的時間, 所以可以做一些處理 > - 打包: > - html 檔中訪問太多 js 檔了, 每個 js 檔都要經歷收發請求的過程 > - 解決辦法其實就是把所有 js 檔包成一包 > - 注意事項: > - 記得加 ; > - 順序要對 ```javascript= // tools ;(function () {})() // candy ;(function () {})() // snake ;(function () {})() // game ;(function () {})() // main ;(function () {})() ``` > - 壓縮: > - 就..Google 線上壓縮 js , > - 然後把打包好的js檔丟上去拿到壓縮後的檔案來用 > - 壓縮其實就是把那些註釋, 空格, 甚至改變變量的命名( 命名越長則佔用空間越大 ) > - [在線工具](https://tool.oschina.net/jscompress) ### 自調用參數 ```javascript= ;(function (window, undefined){ var xx = 0; window.xx = xx; })(window, undefined) ``` > - window > - 通常 自調用函數 在最後會有 `window.xx = xx` 來讓用戶訪問內容 > - 而為了讓 `window.xxx` 的 window 能被壓縮, 所以會傳 window 實參進去 > 讓 window 形參指向 window, 此時形參名就可以被壓縮了 > `;(function(w,u){var a=0;w.a=a;})(window,undefined)` > - undefined > - 舊版本的 undefined 可以被賦值, 為了避免這種情形, 所以把 undefined 傳進去 ### bind() > #### 前情提要 ```javascript= function fn() {} console.dir(fn); // function 也是對象, 所以他也有__proto__那套 ``` > - ES5 後在function 的 \_\_proto__ 新增了一個 bind 方法 > - bind() 可以新建一個方法並返回址, 讓方法的 `this` 指向 bind() 的第一個參數 > - 如果要傳參數, 就從第二個參數接著寫即可 > - 這東西看起來跟 python 的 \_\_init__(self) 很像 ```javascript= var a = 100; // window.a = 100 function fn(x,y) { // window.fn = function(){console.log(this.a)} console.log(this.a); console.log(this); console.log(x + y); } fn(1,2) // 100 // window -> window.fn() -> this 當然指向 window // 3 var b = {a:200}; var fn = fn.bind(b, 1, 2); // 創建一個對象, // 讓新對象的 this 指向bind的第一個參數(b) // 返回新對象的址 fn(); // 200 // {a: 200} -> b.fn() -> this 指向 b // 3 ``` `$ vi test.py` ```python class Test(): def __init__(self, x, y): # self 指向 Test self.n1 = x self.n2 = y def add(self): print(self.n1 + self.n2) t = Test(1,2) t.add() # 3 ``` > - 實例 ```javascript= // 這是貪吃蛇感應方向鍵的方法 function keyDirection() { document.onkeydown = function (e) { // console.log(e.keyCode); // 先測試一下上下左右是多少 switch (e.keyCode) { case 38: this.snake.direction = 'top'; break; case 40: this.snake.direction = 'bottom'; break; case 37: this.snake.direction = 'left'; break; case 39: this.snake.direction = 'right'; break; } }.bind(that); // 讓 this 指向 that } ``` ### call() `call([thisArg[, arg[, arg[, ...]]]])` > - 調用函數且改變 this 指向 > - `call()` 的參數用法跟 `bind()` 相同 > - 第一個參數為 this 的指向 > - 後面開始為你要用的實參 > - vs. `bind()` > - `bind()` 只返回一個新函數, 而不調用函數 ```javascript= var a = 100; // window.a = 100 function fn(x,y) { // window.fn = function(){console.log(this.a)} console.log(this.a); console.log(this); console.log(x + y); } var b = {a:200}; /* bind() var fn = fn.bind(b, 1, 2); fn(); // 200 -> b.fn() -> this 指向 b */ fn.call(b, 1,2); // this -> b // 200 // b.a // {a: 200} // b // 3 ``` ## ES6 類 ### 創建類 ```javascript= class 類名 {} ``` ### 實例類 ```javascript= var tmp = new 類名(); ``` ### constructor > - 實例類時, 就會自動調用這個函數(即使沒寫也會) > - constructor 可以接收 new 的參數, 並返回實例對象 > - 類裡面的函數沒有 `function`, 類也沒有`()` > - 看起來跟 python 的 \_\_init__ 有點像 ```javascript= class Name { constructor(name, age) { this.name = name; this.age = age; } } var godjj = new Name('GodJJ', 20); console.log(godjj); ``` ### 類的方法 > - 方法間別加逗號 ```javascript= class Name { constructor(name, age) { this.name = name; this.age = age; } play(where) { console.log(this.name + '打' + where); } } var godjj = new Name('GodJJ', 20); godjj.play('ad'); // GodJJ打ad ``` > - 創建時就調用方法 ```javascript= class Father { constructor(x,y) { this.x = x; this.y = y; this.sum(); // 這句話做了兩件事, // 1. 將 sum 傳到 constructor 對象中 // 2. 調用 sum } sum() { console.log(this.x + this.y); } } new Father(1,2); ``` ### 類的繼承 > - `class a extends b` 即讓 a 繼承 b > - `super()` 會調用父類的構造函數(constructor), 而讓 this 指向子類 ( 調用者 ) ```javascript= class Father { constructor(x,y) { this.x = x; this.y = y; } sum() { console.log(this.x + this.y); } } class Son extends Father { constructor(x,y) { // 這裡的 this 是指向 Son.constructor // 所以傳值到 Son 的值是無法把值傳進 Father 用的 // 導致 son.sum(); 時會沒有值而報錯 // this.x = x; // this.y = y; // super 就相當於 Function.call() // 調用父類的 constructor , 並將 this 指向 Son super(x,y); } } var son = new Son(1,2); son.sum(); ``` > - 既然 super 調用後會讓 this 指向子類 > - 那麼當然 super 也能讓子類調用父類的方法 ```javascript= class Father { test() { console.log('爸爸測試一下'); } } class Son extends Father { test() { // console.log('兒子測試一下') super.test() } } var son = new Son(); son.test(); // 爸爸測試一下 ``` > - super 必須放到 this 之前 ```javascript= class Father { constructor(x,y) { this.x = x; this.y = y; } sum() { console.log(this.x + this.y); } } class Son extends Father { constructor(x,y) { super(x,y); this.x = x; this.y = y; // super(x,y); -> 放這裡會報錯 } sub() { console.log(this.x - this.y); } } var son = new Son(5,3); son.sum(); son.sub(); ``` ### 注意事項 > - 類不會 hoisting ```javascript= var f = new Father(); class Father {} // Cannot access 'Father' before initialization ``` > - 類的 this 指向 > - constructor 的 this 指向類實例對象 > - 方法裡的 this 指向調用者 ```javascript= var that; var thaat; class Father { constructor() { console.log(this); that = this; } tmp() { console.log(this); thaat = this; } } var fa = new Father(); // Father {} // constructor 的 this 指向實例對象 fa.tmp(); // Father {} // 方法裡面的 this 指向調用者 // 實例對象調用, this 當然指向該對象 console.log(that === fa); // true console.log(thaat === fa); // true ``` > - 陷阱題: ```htmlmixed= <body> <button id='btn'></button> <script src='tmp.js'></script> </body> ``` ```javascript= var that; class Father { constructor(name) { that = this; this.name = name; this.btn = document.querySelector('#btn'); this.btn.onclick = this.tmp; } tmp() { console.log(this); console.log(this.name); console.log(typeof this.name); console.log(that.name); } } var fa = new Father('GodJJ'); fa.tmp(); // 輸出結果 // Father {name: "GodJJ", btn: button#btn} -> fa 調用的 // GodJJ -> 輸出 fa.name // string // GodJJ -> that 指向 constructor 創建的對象 ( fa的指向 ) // 打擊 button 後 輸出結果 // <button id='btn'></button> -> 觸發事件的事 btn, 也就是調用tmp的是 btn // -> 這個 this 是指向 DOM, 而 DOM 沒有 name 屬性, 所以是空的 // string -> 但好像又不是 undefined, 是一個空 str // GodJJ -> 如果想要點擊後使用 constructor 的屬性, 那就可以用 that 存起來那招 // 實際上 fa 對象裡裝的東西 // btn: button#btn // name: 'GodJJ' // __proto__: Father.prototype ``` ### 實作: tab 欄 ![](https://i.imgur.com/qrjwlEn.png) > - 點擊tab 會切換 > - 點擊 x 會刪除 > - 點擊 + 會新增 > - 雙擊內容可以更新 > - 細節都在注釋, 刪除功能有三行非常精彩 ```htmlmixed= <head> <meta charset='utf-8'> <link rel='stylesheet' href='css/style.css'/> </head> <body> <div class='tabBox'> <div class='tabTop clearfl'> <ul class='clearfl'> <li class='curTab'><span>1</span><i>x</i></li> <li><span>2</span><i>x</i></li> <li><span>3</span><i>x</i></li> </ul> <div class='addBtn'> <span>+</span> </div> </div> <div class='tabBody'> <section class='curBody'><span>1</span></section> <section><span>2</span></section> <section><span>3</span></section> </div> </div> <script src='js/tab.js'></script> </body> ``` ```css= * { margin: 0; padding: 0; } i { text-decoration: none; font-style: normal; } ul { list-style: none; } .tabBox { height: 600px; width: 800px; border: 1px solid orange; margin: 100px auto; } .clearfl:before, .clearfl:after { content: ''; display: table; } .clearfl:after { clear: both; } .tabTop ul { float: left; } .tabTop li { float: left; line-height: 30px; width: 100px; border-right: 1px solid gray; border-bottom: 1px solid gray; text-align: center; position: relative; } .tabTop li i { position: absolute; display: block; line-height: 10px; font-size: 8px; width: 10px; color: white; background: black; top: 0; right: 0; border-radius: 0 0 0 100%; } .tabTop .curTab { border-bottom: 0; } .addBtn { float: right; line-height: 20px; width: 20px; border: 1px; margin-right: 5px; margin-top: 8px; border: 1px solid gray; text-align: center; } .tabBody { height: 569px; width: 100%; padding: 10px; box-sizing: border-box; position: relative; } .tabBody section { height: 100%; width: 100%; position: absolute; display: none; } .tabBody .curBody { display: block; } ``` ```javascript= var that; // 很多事件觸發, 如此要操作 constructor 就有困難, 所以開 that class Tab { constructor(cls) { that = this; this.main = document.querySelector(cls); this.ul = this.main.querySelector('ul'); this.addBtn = this.main.querySelector('.addBtn'); this.tabBody = this.main.querySelector('.tabBody'); this.init(); } init() { // 獲取元素 this.getLisSecElem(); // 添加事件 for (var i = 0; i < this.lis.length; i++) { this.lis[i].index = i; // 存位置, 等等比較好做事 this.lis[i].onclick = this.switchTab; this.closes[i].onclick = this.delTab; this.spans[i].ondblclick = this.updateTab; this.sections[i].ondblclick = this.updateTab; } this.addBtn.onclick = this.addTab; } // 把為了每次動態添加都需要重新獲取的元素抽出來 // 因為 querySelector 不是動態集合 // 如果適用 get 系列好像就不用這樣搞剛 getLisSecElem() { this.lis = this.main.querySelectorAll('li'); this.sections = this.main.querySelectorAll('section'); this.closes = this.main.querySelectorAll('i'); this.spans = this.main.querySelectorAll('.tabTop li span'); } // 點擊切換 switchTab() { that.clearClsName(); // 排他, 因為很常用, 所以拿出去寫 this.className = 'curTab'; that.sections[this.index].className = 'curBody'; } clearClsName() { for (var i = 0; i < that.lis.length; i++) { that.lis[i].className = ''; that.sections[i].className = ''; } } addTab() { /* 要改內容還要找span, 太麻煩了 * 用 create 那個更麻煩, 要開超多東西 var cloneLi = that.lis[0].cloneNode(true); var span = cloneLi.querySelector('span'); span.innerText = 'tmp'; */ that.clearClsName(); // 排他, 這是事件方法(點擊+), this 是指向 + var random = Math.random(); // 為了看 span.innerText 內容有沒有改, 測試用的 // elem.insertAdjacentHTML(position, text) // 用法補在WebAPI那 // 直接寫 str 插到父元素就好了 var li = '<li class="curTab"><span>tmp</span><i>x</i></li>' var section = '<section class="curBody"><span>'+ random + '</span></section>' that.ul.insertAdjacentHTML('beforeend', li); that.tabBody.insertAdjacentHTML('beforeend', section); that.init(); // 因為querySelector() 不是動態集合, 所以每次添加元素後都要重新獲取元素 } // 點擊刪除 delTab(e) { // 取消冒泡, 避免觸發 li 的切換事件 e.stopPropagation(); var index = this.parentNode.index; // 取得被點擊的編號 // 刪除元素 that.lis[index].remove(); // node.remove() 指定元素刪除 that.sections[index].remove(); that.init(); // 重整一下 // 如果正在選取狀態的那個被刪掉了, 他的前一個變成選定狀態 /* 太麻煩了, 刪除之後直接執行點擊前一個的操作即可 that.clearClsName(); that.lis[that.lis.length - 1].className = 'curTab'; that.sections[that.sections.length - 1].className = 'curBody'; */ // 下面這幾步, 非常厲害 // 1. 如果有 curTab 這個類, 表示不是刪掉現在選定的元素, 那就不用操作點擊 if (document.querySelector('.curTab')) {return} // 2. 點擊前一個, 所以編號 -1 index--; // 3. 如果前一個有東西, 就點擊(手動調用觸發) // 避免刪除第一個時, 選取 [-1] 的元素 that.lis[index] && that.lis[index].click(); } // 雙擊更新 updateTab(){ // 雙擊會選取文字, 這串是取消選取文字 window.getSelection? window.getSelection().removeAllRanges(): document.selection.empty(); // 把原本的內容存起來 var str = this.innerHTML; // 把內容改成 input this.innerHTML = '<input type="text"/>'; // 調整一下css this.children[0].style.width = '100%'; this.children[0].style.boxSizing = 'border-box'; // 把觸發前的內容寫進input this.children[0].value = str; // 觸發時選中文本框內的文字 this.children[0].select(); // 把內容改成輸入後的東西 this.children[0].onblur = function () { this.parentNode.innerHTML = this.value; } // 案 ENTER 時, 取消焦點 this.onkeyup = function (e) { // console.log(e.keyCode) // ENTER: 13 if (e.keyCode === 13) { this.children[0].blur(); // 手動調用取消焦點 } } // 這邊其實觸發的可以改成外層的 li 比較好, 但是這樣就要改裡面的標籤 // 會比較麻煩, 所以先算了, 等我有空, I will be back } } var tab = new Tab('.tabBox'); ``` ## 繼承 > - 讓子類共同的東西拿到父類來, 重用父類 > - 不過因為 JS 繼承比較麻煩, 如果真的有很多共同的再使用即可 ### 對象拷貝 > - 遍歷繼承目標 > - 查看被繼承目標有無 > - 複製 ```javascript= var lol1 = { name: 'GodJJ', age: 20, play: function () { console.log('AD'); }, } var lol2 = { : 'bebe', } function extend(parent, child) { // 遍歷所有父元素 for (key in parent) { // 如果子元素有值, 就不繼承 if (child[key]) { continue; } child[key] = parent[key]; // 複製 } } console.log(lol2); // {name: "bebe"} extend(lol1, lol2); console.log(lol2); // {name: "bebe", age: 20, play: ƒ} ``` > - 貪食蛇改 ```javascript= if (candy.x == headX && candy.y === headY) { var lastBox = obj[obj.length-1]; /* 原本的 obj.push({ x: lastBox.x, y: lastBox.y, color: lastBox.color, }) */ // 對象拷貝 var tmp = {} extend(lastBox, tmp); obj.push(tmp); candy.render(parent); } ``` ### 原型繼承 > - 改變 prototype 到父類實例, (注意! 不是父類原型) ![](https://i.imgur.com/kFTQcWX.jpg) > - 缺點: 無法從實例中改變父類的值 ```javascript= function Fruit(store, origin) { this.store = store; this.origin = origin; } function Mango(price) { this.price = price; } // 把原型改成指向父類實例 Mango.prototype = new Fruit('全聯', '台南'); Mango.prototype.constructor = Mango; // 改變原型指向時, 記得 Who you are var mango = new Mango(20); // 無法從這裡改變父類的值 console.dir(mango); console.log(mango.store) // 全聯 ``` > - 別直接把子類的原型直接指向父類原型 > - 因為後面再更改時是直接改父類原型的值 ```javascript= function Fruit(store, origin) { this.store = store; this.origin = origin; } function Mango(price) { this.price = price; } Mango.prototype = Fruit.prototype // 把 Mango 原型指向 Fruit原型 Mango.prototype.constructor = Mango; // 動態修改 var mango = new Mango(20); function Apple(price) { this.price = price; } Apple.prototype = Fruit.prototype // Apple 原型也指向 Fruit 原型 Apple.prototype.constructor = Apple; // 動態修改 var apple = new Apple(20); // 發生悲劇 console.log(mango.constructor); // ƒ Apple(price) {this.price = price;} // 因為Fruit.prototype, Mango.prototype, Apple.prototype 都指向同個對象 ``` ### 借用構造函數 > - 調用父元素, 並利用 `call()` 來改變 this 指向 > - 缺點: 無法借用到 prototype 裡的屬性方法 ```javascript= function Fruit(store, origin) { this.store = store; this.origin = origin; } Fruit.prototype.shop = function () { console.log('去' + this.store + '買'); } function Mango(store, origin, price) { // new Mango()進來時 // store = '全聯' // origin = '台南' // price = 20 // this = Mango Fruit.call(this, store, origin) // -> 原本調用時 this 指向 window, // -> 用 call 改變 this 指向 new Mango() // Mango.store = '全聯'; // Mango.origin = '台南'; this.price = price; // Mango.price = 20 // return Mango } var mango = new Mango('全聯', '台南', 20); console.dir(mango); console.log(mango.store) // 全聯 mango.shop(); // ERROR // 缺點: 只能借用function本身有的東西, 沒辦法借用 prototype 裡的東西 ``` ### 組合繼承 > - 其實就是借用 + 原型 ```javascript= function Fruit(store, origin) { this.store = store; this.origin = origin; } Fruit.prototype.shop = function () { console.log('去'+ this.store + '買'); } function Mango(store, origin, price) { Fruit.call(this, store, origin); // 借用 this.price = price; } Mango.prototype = new Fruit(); // 原型 Mango.prototype.constructor = Mango; var mango = new Mango('全聯', '台南', 20); mango.shop(); // 去全聯買 console.log(mango.origin); // 台南 ``` ## 函數 ### 函數定義方式 > - 函數聲明 > - 函數表達式 > - `new Function` > #### 函數聲明 ```javascript= function fn() {} ``` > #### 函數表達式 ```javascript= var fn = function () {} ``` > #### 區別: hoisting 方式不同 > - 函數聲明是整個都 hoistion上去 > - 函數表達式會拆成聲明與賦值 ```javascript= fn(); // 1 function fn() { console.log(1); } fn2(); // ERROR var fn2 = function () { console.log(2); } ``` ```javascript= // hoisting 後 function fn() { console.log(1); } var fn2; fn(); // 1 fn2(); // ERROR -> 此時 fn2 是 undefined fn2 = function () { console.log(2); } ``` ### hoisting 補充 > #### 舊版本瀏覽器的 if 也會 hoisting ```javascript= if (true) { function fn() {console.log(1)}; } else { function fn() {console.log(2)}; } fn(); ``` > - 以上這段 code 在瀏覽器的結果是 2 ```javascript= // 推測他的執行過程應該是 function fn() {console.log(1)}; function fn() {console.log(2)}; // 後面把 fn 重新指向了 if (true) {} else {}; // 跟這判斷式無關 fn(); ``` > - 解決辦法 ```javascript= if (true) { var fn = function () { // hoisition 變量而已 console.log(1) }; } else { var fn = function () { console.log(2) }; }; fn(); ``` > #### `new Function()` > - 執行速度相較於其他調用方法慢 > - 因為解析次數較多 ```javascript= function fn2() { var a = 100; console.log(a); } // 上述全部都是 js 語言, 直接解析成 os 語言即可 var fn = new Function('var a = 100; console.log(a);'); // 首先要先將參數的 str 解析成 js 語言, 然後實例後又要再解析成 os 語言 // 所以執行速度比較慢 fn2(); // 100 fn(); // 100 ``` > - 傳參數的方法: 形參在前面, 表達式在後面 ```javascript= function fn2(x,y) { console.log(x,y); console.log(typeof x, typeof y); } var fn = new Function('x', 'y', 'console.log(x,y);console.log(typeof x, typeof y);'); fn2(1,2); // 1 2 // num num fn(1,2); // 1 2 // num num ``` > - 不論函數聲明還是函數表達式, 都是 Function 的實例 ```javascript= var fn = function () {}; function fn2() {}; console.log(fn.__proto__ === Function.prototype); // true console.log(fn2.__proto__ === Function.prototype); // true ``` ### 函數調用與 this 指向 ```javascript= // 普通函數調用 // this 指向 window function fn() { console.log(this); } fn(); // window -> window.fn() // 方法調用 // this 指向調用方法的對象 var obj = { fn1: function () { console.log(this); }, } obj.fn1(); // {fn1: ƒ} // 構造函數調用 // this 指向實例對象 function Cls() { this.fn = function () {console.log(this);}; } var cls = new Cls(); cls.fn(); // Cls {fn: ƒ} // 事件綁定 // 觸發事件的對象 var btn = document.getElementById('btn'); btn.onclick = function () { console.log(this); // <button id='btn'></button> } // 定時器函數 // this 指向 window setInterval(function () { console.log(this); // window -> window.setInterval }, 1000) // 自調用函數 ;(function (){ console.log(this) // Window })() ``` > - 老話一句, 誰調用, this 指向誰 ```javascript= function fn () { console.log(this); } fn(); // Window var obj = { fn: fn, } obj.fn(); // {fn: ƒ} // ----------------------------------- // var obj2 = { fn2: function () { console.log(this); } } obj2.fn2(); // {fn2: ƒ} var fn = obj2.fn2; fn(); // Window function Cls() { this.fn = function () {console.log(this);}; } Cls.prototype.tmp = function () {console.log(this);}; var cls = new Cls(); cls.fn(); // cls Cls.prototype.tmp(); // Cls.prototype ``` ### call(), bind(), apply() > - 三者主要皆用來改變 this 指向 > - `call()` 跟 `apply()` 都是用來立即調用函數 > - `call()` 的參數為參數列表(,) > - `apply()` 的參數為陣列([,]) > - `bind()` 與其他兩個最大的區別在於, bind()不會調用函數, 而是創建一個拷貝函數 > #### `call()` > - `fn.call(thisArg[, arg1[, arg2[, ...]]])` > - 調用函數 > - 改變調用函數的this指向為第一個參數 > - 如果傳 `null` 表示不改變 this 指向 > - 不過最好不要傳 `null` 否則可能會產生問題, > 如果不改指向, 就傳原本的指向即可 > - 後面參數就一般實參 > - 返回值就是函數的返回值 > - 主要常用於繼承, 調用父構造函數, 並改變指向為子構造函數 (window -> son) ```javascript= function Father(x,y) { this.x = x; this.y = y; } function Son(x,y) { Father.call(this, x, y); this.sum = this.x + this.y; } var son = new Son(1,2) console.log(son.sum); // 3 // ------不改變指向------------- // function fn() { console.log(this); } // fn.call(null); // Window fn.call(window); // 應用: 非陣列調用陣列方法 var obj = { // 偽陣列 0: 100, 1: 80, length: 2, } // obj['2'] = 30; // obj.length++; // console.log(obj); // {0: 100, 1: 80, 2: 30, length: 3} Array.prototype.push.call(obj, 30); console.log(obj); // {0: 100, 1: 80, 2: 30, length: 3} ``` > #### `apply()` > - `fn.apply(thisArg, [argsArray])` > - 第一個參數為 this 指向 > - 第二個參數為一個陣列實參 > - 被調用的函數會將實參裡的值拆成一個一個, 分散到每個形參中 > - 會直接調用函數 > - 返回值就是函數的返回值 > - 應用: 常用於補充陣列沒有的方法, 類似借用繼承的概念 ```javascript= function fn(a,b,c) { console.log(this); console.log(a); console.log(typeof a); console.log(b); console.log(typeof b); console.log(c); } var arr = ['haha', 1] var v = {}; fn.apply(v, arr); // {} -> this 指向 // haha -> a // string -> a 的類型 // 1 -> b // number -> b 的類型 // undefined -> c // 應用 var arr2 = [22,33,64,99]; var max = Math.max.apply(Math, arr2); // 沒有用到 this 就只回去就好了 console.log(max); // 99 // 應用 var arr = [1,2,4,6]; console.log(arr); // (4) [1, 2, 4, 6] console.log.apply(console, arr); // 1 2 4 6 ``` > #### `bind()` > - `fun.bind(thisArg[, arg1[, arg2[, ...]]])` > - 參數跟 `call()` 一模一樣 > - `bind()` ==不會調用函數==, > - `bind()` 返回一個用 bind 參數改造的==原拷貝函數== > - 需求: 按鈕後關閉, 三秒後打開 ```htmlmixed= <body> <button></button> <button></button> <button></button> <script src='test.js'></script> </body> ``` ```javascript= var btns = document.querySelectorAll('button'); console.log(btns); for (var i = 0; i < btns.length; i++) { btns[i].onclick = function () { this.disabled = true; // var that = this setTimeout(function () { // btns[i].disabled = false; // Cannot set property 'disabled' of undefined // 因為這個事件是後來觸發的, i = 4了, 所以 btns[4] undefined // that.disabled = false; // 用 that 可以, 只是還要另外在開記憶體 this.disabled = false; // 如果沒有 bind(), // 那 this 指向的是 window, 按鈕永遠都不會開 }.bind(this), 3000) } } // 另外想法 // 這題用 forEach 也能解, 因為第一個參數就一直指向那個對象不會變 btns.forEach(function (v) { v.onclick = function () { this.disabled = true; setTimeout(function () { v.disabled = false; // v 就一直指著那個按鈕, 所以不會有 undefined 的問題 }, 3000) } }) ``` > - 應用2: 如果一個函數同時需要用到兩種 this ```htmlmixed= <body> <button></button> <script src='test.js'></script> </body> ``` ```javascript= // var that; function Fn(a) { // that = this; this.a = a; this.btn = document.querySelector('button'); this.tmp = function (that) { console.log(this); // 同時需要用到點擊的 DOM this console.log(that); // 跟實例對象的this // 有個做法是在外面開一個變量後, 在裡面存實例對象 // 但是這樣這個函數就一直需要外面的變量才能用 // bind()就可以解決這個問題 } this.btn.onclick = this.tmp.bind(this.btn, this); // 綁定不能變, 否則弄不到被點擊的 DOM 對象, // 實例對象的 this 就直接當實參傳進去, 對那個 function 使用即可 } var f = new Fn(1); ``` ### 函數其他屬性 ```javascript= function fn(x,y) { console.log(fn.arguments); // 偽陣列, 獲取調用的實參 console.log(fn.caller); // 函數調用者, 全局調用時為 null console.log(fn.length); // 形參個數 console.log(fn.name); // 函數名, string } function tmp() { fn(1,2,3) } console.dir(fn); tmp(); // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] // ƒ tmp() {fn(1,2,3)} // 2 // fn ``` ```javascript= function fn() { console.log(fn.arguments); // 偽陣列, 獲取調用的實參 console.log(arguments); // 函數內部有個私有變量, 看起來跟上面那個一樣功能 } fn(1,2,3); // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] // arguments 應用 function getMax() { let max = arguments[0]; for (let i = 1; i< arguments.length; i++) { if (max < arguments[i]) { max = arguments[i]; } } return max } let max = getMax(10,2,6,99,10); console.log(max); // 99 // 延伸: ES6 剩餘參數 function getMax2(...args) { let max = args[0]; for (let i = 1; i< args.length; i++) { if (max < args[i]) { max = args[i]; } } return max } let max2 = getMax2(10,2,6,99,10); console.log(max2); // 99 ``` ### 高階函數 > - 其實就是指函數可以當球踢 > - 把函數當實參傳進去, 例如 callback > - 把函數 return 出來 > #### callback ```javascript= function fn() { return function () {console.log(2)} } function fn2(func) { func && func(); } fn()(); // 2 fn2(function () {console.log(1);}) // 1 ``` ```javascript= // sort() let arr = [3,6,11,8,33,10, 99, 1]; arr.sort((a,b)=>a-b); // callback console.log(arr); // (8) [1, 3, 6, 8, 10, 11, 33, 99] // 複習冒泡 Array.prototype.bubbleSort = function (fn) { for (let i = 0; i < this.length - 1; i++) { // console.log(i); let isSort = true; for (let j = 0; j < this.length - i - 1; j++) { // console.log(this); if (fn(this[j], this[j+1]) > 0) { isSort = false; let tmp = this[j]; this[j] = this[j+1]; this[j+1] = tmp; } } if (isSort) { break } } } let arr = [3,6,11,8,33,10, 99, 1]; arr.bubbleSort((a,b) => a - b) // callback console.log(arr); // (8) [1, 3, 6, 8, 10, 11, 33, 99] ``` > #### return ```javascript= // 生成 [1~10] 隨機數 function getRandom() { return Math.floor(Math.random() * 10 + 1) } console.log(getRandom()); console.log(getRandom()); console.log(getRandom()); // 使用第一次生成的隨機數 function useRandom() { // 這個函數只未了生成隨機數並保存 let num = Math.floor(Math.random() * 10 + 1); return function () { // 這個函數只為了輸出 num return num } } let fn = useRandom(); console.log(fn()); console.log(fn()); console.log(fn()); // 求 n + m // n 變動小, m 變動大 function func(n) { // 變動小的存起來 return function (m) { return n + m } } let sum100 = func(100); let sum1000 = func(1000); console.log(sum100(10)) console.log(sum100(40)) console.log(sum1000(20)) console.log(sum1000(30)) ``` ### 閉包 (closure) > - 形式: 函數嵌套函數, 內函數訪問了外函數的變量或函數 > - 函數有權訪問另外一個函數作用域的變量 > - 主要作用: 延伸函數裡變量作用範圍 ( 本來只有局部作用域能用 ) > - 缺點: 由於沒有釋放記憶體, 所以比較肥 > - 子函數訪問父函數 ```javascript= function fn() { var a = 10; function fn2() { console.log(a); // fn2 本身沒有 a, 他拿的 a 是 fn 的 } fn2(); } fn(); // 10 ``` > - 全局訪局部 ```javascript= function fn() { var a = 10; // function fn2() { // console.log(a); // } // return fn2; return function () { // 簡化 console.log(a); } } var f = fn(); // 返回的函數還指向著父函數裡的變量, 所以 fn() 開起的作用域沒有被回收 // 也就延展了變量的作用範圍 f(); // 比較 function fn2() { var b = 10; return b } fn2() // 執行完即銷毀 ``` > - 點擊輸出索引值 ```htmlmixed= <body> <div>1</div> <div>2</div> <div>3</div> <script src='test.js'> </script> </body> ``` ```javascript= var divs = document.querySelectorAll('div'); /* 一般的循環註冊 for (var i = 0 ; i < divs.length; i++) { divs[i].index = i; divs[i].onclick = function () { console.log(this.index); } } */ /* divs.forEach(function (v,i) { v.index = i; v.onclick = function () { console.log(this.index); } }) */ // 閉包寫法 for (var i = 0; i < divs.length; i++) { (function (i) { divs[i].onclick = function () { console.log(i); // 這個 fn 沒有 i, 但是父函數的形參有 -> 用別人的: 閉包 } })(i) // i 剛好等於索引值, 把 i 傳進去使用 } // 三秒後輸出各自的內容 for (var i = 0; i< divs.length; i++) { (function (elem) { setTimeout(function () { console.log(elem.innerHTML) // 使用父函數的形參 }, 3000) })(divs[i]) // 把元素傳進去用 } ``` > - 兩個函數的指向思考 ```javascript= var name = 'window'; var obj = { name: 'obj', fn: function () { return function tmp() { return this.name } }, fn2: function () { var that = this; return function tmp2() { return that.name } } } console.log(obj.fn()()); // window // obj.fn() 返回一個函數到全局 tmp() // 這個 tmp() 的父函數沒有任何變量聲明的操作, // 且, this 指向的是全局, 而沒有使用任何父函數的變量, 故沒有閉包產生 // 普通模式下直接調用 tmp() -> window.tmp() console.log(obj.fn2()()); // obj // obj.fn2() 執行了一個宣告附值 this 的操作, // 此時的 this 指向調用者的 obj, 附值給了 that // 然後返回了一個函數 tmp2 到全局 // tmp2() 調用時, 雖然 this 一樣指向 window, // 可是使用的是剛剛產生的閉包變量 that ``` ### 遞歸 > - 在函數內部調用自己 > - 效果跟 while 很像, > - 當然注意點也差不多, 就是避免造成死循環(stack overflow), 在條件達成時要退出 ```javascript= var num = 1 function fn() { console.log(1); num++ if (num>3) { return } fn() // 在裡面調自己 } fn() ``` > - 求 1+2+...+n ```javascript= /* var num = 1 var sum = null; function fn(n) { sum+=num; num++ if (num>n) { console.log(sum); return } fn(n) } */ function fn(n) { if (n == 1) { return 1 } return n + fn(n-1) // -> n-1 參數傳進去就是每圈 n--了 } console.log(fn(10)); // 55 ``` > - 求 1\*2\*3...\*n ```javascript= function fn(n) { if (n == 1) { return 1 } return n * fn(n-1) } console.log(fn(3)); // 6 // 假設 n = 3 // fn(3) -> return 3 * fn(3-1) // fn(2) -> return 2 * fn (2-1) // fn(1) -> 1 // fn(3) -> return 3 * 2 * 1 ``` > - 求 Fibonacci ```javascript= // fibonacci: 第三個數等於前兩個和 // 1 1 2 3 5 8 13 /* function getFb(n) { var fir = 1; var sec = 1; var fb; for (var i = 3; i<=n; i++) { fb = fir + sec; fir = sec; sec = fb; } return fb } */ function getFb(n) { if (n == 1 || n == 2) { return 1 } return getFb(n-1) + getFb(n-2) } console.log(getFb(3)); // 2 // getFb(2) + getFb(1) = 1 + 1 = 2 console.log(getFb(5)) // 5 // getFb(4) + getFb(3) // getFb(3) + getFb(2) + getFb(2) + getFb(1) // getFb(2) + getRb(1) + 1 + 1 + 1 = 5 ``` > - 輸入 id 查資料 ```javascript= function getData(json, n) { var o = {}; json.forEach(function (v) { if (v.id == n) { o = v; o.flag = 1; // 為了避免後面圈數進去elif後返回一個 {} 出來讓o 被重新指向 // 暫時還沒想到更好的解法 // I will Be back } else if (v.team && v.team.length > 0) { if (getData(v.team, n).flag == 1) { o = getData(v.team, n); } } }) return o } console.log(getData(datas, 1)); console.log(getData(datas, 2)); console.log(getData(datas, 11)); console.log(getData(datas, 22)); ``` ## 淺拷貝與深拷貝 ### 淺拷貝 > - 淺拷貝只拷貝一層, 第一層資料如果是對象, 拷貝的是該對象的引用 > - 所以如果操作資料是對象的對象, 那麼原本被拷貝的資料也會跟著變, > 因為大家都指向同一個對象 > <img src='https://i.imgur.com/wiAzFu3.jpg' style='width: 300px'/> ```javascript= var obj = { id: 1, name: 'GodJJ', play: { po: 'AD', lev: 'GE', } } var o = {}; for (k in obj) { o[k] = obj[k]; } // console.log(o); // id: 1 // name: "GodJJ" // play: {po: "AD", lev: "GE"} // __proto__: Object o.id = 2; console.log(obj.id, o.id); // 1 2 o.play.po = 'mid'; console.log(obj.play.po, o.play.po); // mid mid ``` ### ES6 的淺拷貝 `Object.assign(target, ...sources)` ```javascript= var obj = { id: 1, name: 'GodJJ', play: { po: 'AD', lev: 'GE', } } var o = {}; Object.assign(o, obj); console.log(o); // id: 1 // name: "GodJJ" // play: {po: "AD", lev: "GE"} // __proto__: Object ``` ### 深拷貝 ```javascript= var obj = { id: 1, name: 'GodJJ', tmp: [2,3,4], play: { po: 'AD', lev: 'GE', } } var o = {}; function deepCopy(target, source) { for (k in source) { var item = source[k]; // 把資料拿出來 if (item instanceof Array) { // 如果資料是 Array (註1) target[k] = []; // 創一個新的陣列對象 deepCopy(target[k], item); // 再進去掃一次 (註2) // item = [2,3,4] // for (k in item) // {0: 2, 1: 3, 2: 4} } else if (item instanceof Object) { // 如果資料是物件 target[k] = {}; // 創一個新的物件對象 deepCopy(target[k], item); // 進去掃 } else { target[k] = item; // 掃到不是物件類型就存進去 } } } deepCopy(o, obj); // console.log(o); // id: 1 // name: "GodJJ" // play: {po: "AD", lev: "GE"} // tmp: (3) [2, 3, 4] // __proto__: Object // 修改拷貝數據不影響原資料 // 因為拷貝的對象數據指向的是自己新創建的對象, 不是原本的 o.tmp[0] = 100; o.play.po = 'Mid'; console.log(obj.tmp, o.tmp); // (3)[2, 3, 4] (3) [100, 3, 4] console.log(obj.play.po, o.play.po); // AD Mid ``` ```javascript= // 註1: 陣列也是物件對象, 故先判斷是不是陣列, 再判斷是不是物件 var a = []; console.log(a instanceof Object); // true // 註2: 陣列複習 for (k in a) { console.log(k, a[k]); // 0 2 // 1 3 // 2 4 } a[4] = 100; console.log(a); // (5) [1, 2, 3, empty, 100] ``` ### 遍歷 DOM 樹 > - 找到所有子元素, 並對子元素做callback ```htmlmixed= <body> <div>HaHa</div> <ul id="tmp"> <li>1</li> <li>2</li> <li>3</li> </ul> <button>GO</button> <script src='test.js'></script> </body> ``` ```javascript= function getElem(parent, callback) { // 遍歷子元素 for (var i = 0; i < parent.children.length; i++) { var child = parent.children[i]; // console.log(child); callback && callback(child); // 對子元素作 callback getElem(child); // 遍歷子元素的子元素 } } getElem(document.body.querySelector('ul'), function (elem) { elem.onclick = function () { console.log(this.innerText); } }); ``` ## 嚴格模式(strict mode) > - 簡單說就是讓 JS 執行code嚴謹一點 > - IE10 + 才支援 ### 嚴格模式的開啟 > - 在所有code之前寫上 `'use strict';` > - 沒有支援的瀏覽器就會當成 str 來略過 ```htmlmixed= <body> <!-- 為整個 script標籤 開啟--> <script> 'use strict'; </script> <!-- 為 自調用函數 開啟 --> <script> ;(function () { 'use strict'; })() </script> <!-- 為 某個函數 開啟 --> <script> function fn() { 'use strict'; // 這個函數執行嚴格模式 } function fn2() { // 這個函數沒有影響 // 執行普通模式 } </script> </body> ``` ### 嚴格模式的變化 > #### 變量 > - 未聲明就使用, Error > - 嚴禁刪除已聲明變量 `delete x;` ```javascript= // 普通模式 a = 10; console.log(a); // 10 -> 未聲明, 可以用 delete a; // -> 可以刪 console.log(a); // Error: a is not defined ``` ```javascript= // 嚴格模式 'use strict'; // a = 10; // a is not defined var a = 10; console.log(a); // delete a; // Delete of an unqualified identifier in strict mode. console.log(a); ``` > #### this > - 全局作用域的函數 this 指向 window, 嚴格模式指向 undefined > - 基於這個原理的應用當然也會掛掉, 其他則沒有影響 > - 不變的定理: 誰調用就指誰, 鬼調用就會掛 ```javascript= // 普通模式 function fn() { console.log(this); } fn(); // window.fn() -> Window {...} function Fn2() { this.a = 1; } Fn2() // window.a = 1; console.log(window.a); // 1 setInterval(function () { console.log(this); // Window {...} }, 1000) ``` ```javascript= // 嚴格模式 'use strict'; function fn() { console.log(this); } fn(); // undefined function Fn2() { this.a = 1; } // Fn2() // undefined.a = 1 // Cannot set property 'a' of undefined at Fn2 // 由於全局函數指向 undefined 當然會報錯 console.log(window.a); // 1 setInterval(function () { console.log(this); // Window {...} // 這本來就是window的方法了, 所以沒有影響 }, 1000) ``` > #### 函數 > - 不允許形參重名 > - 不建議在==非==函數 code 裡聲明函數(沒有強制, 因為我測試沒有掛) ```javascript= 'use strict'; // function fn(a,a) { // Duplicate parameter name not allowed in this context function fn(a,b) { console.log(a+a); } fn(1,2); // 4 // 在非函數裡宣告函式 // 下面都沒掛, 可是 MDN 有寫不要這樣寫 if (true) { function fn2() {} } while (true) { function fn3() {} break } for (var i = 0; i<2; i++) { function fn4() {} } // 在函數裡宣告函式 // 這本來就是合法的 function fn5() { function fn6() {} } ``` ## ES5 新增的方法 ### 陣列 > #### forEach() > - `array.forEach(function([curValue, index, arr]))` > - 用來遍歷陣列 > - 參數是一個 callback > - callback 第一個參數是當時的值 > - callback 第二個參數是當時的位置 > - callback 第三個參數是陣列本身 > - 沒有返回值 ```javascript= var a = [1,2,3]; // 以前遍歷方法 for (var i = 0; i < a.length; i++) { console.log(a[i]); console.log(i); console.log(a); } // forEach var b = a.forEach(function (value, index, array) { console.log(value); console.log(index); console.log(array); }) console.log(b) // undefined ``` > #### filter() > - `array.filter(function(curValue, index, arr))` > - 參數跟 `forEach(callback)` 都一樣 > - `filter()` 主要用於篩選陣列 > - `filter()` 會返回一個新陣列, 篩選條件在 return 寫即可 ```javascript= var a = [11,22,33]; var c = a.filter(function (value) { return value >= 20 }) console.log(c); // (2) [22, 33] ``` > #### some() > - `array.some(function(curValue, index, arr))` > - 參數也跟上面一樣 > - `some()` 返回的是 Boolean > - 只要符合即馬上停止循環, 返回 true ```javascript= var arr = [11,22,33]; var arr2 = ['G', 'O', 'D'] var a = arr.some(function (value){ return value < 10 }) var b = arr2.some(function (value){ return value === 'G' }) console.log(a); // false console.log(b); // true ``` > #### map(), every() > - 暫略 ### 練習: 查詢商品 ```htmlmixed= <head> <meta charset='utf-8'> <link rel='stylesheet' href='css/style.css'/> </head> <body> <div class='box'> <head> <div id='top'> <span>按照價格查詢:</span> <input type='text'/> <span>-</span> <input type='text'/> <input type='button' value='搜索'/> </div> <div id='buttom'> <span>按照商品名稱查詢:</span> <input type='text'/> <input type='button' value='查詢'/> </div> </head> <table> <thead> <tr> <th>id</th> <th>品名</th> <th>價格</th> </tr> </thead> <tbody> </tbody> </table> </div> <script src='js/main.js'></script> </body> ``` ```css= * { margin: 0; padding: 0; } .box { margin: 100px auto; width: 600px; } table { text-align: center; } th, td { height: 50px; width: 100px; border: 1px solid red; } ``` ```javascript= // 資料 var json = [ { id: 1, name: 'apple', price: 20, }, { id: 2, name: 'banana', price: 25, }, { id: 3, name: 'grova', price: 30, } ] // 拿到元素們 var tbody = document.querySelector('tbody'); var price = document.querySelectorAll('#top input[type=text]'); var btn = document.querySelectorAll('input[type=button]'); var n = document.querySelector('#buttom input[type=text]'); // 顯示資料 function render(j) { tbody.innerHTML = ''; j.forEach(function (v) { var tr = document.createElement('tr'); tr.innerHTML = '<td>' + v['id'] + '</td><td>' + v['name'] + '</td><td>' + v['price'] + '</td>'; tbody.appendChild(tr); }); } // 價格區間 btn[0].onclick = function () { var newJson = json.filter(function (v) { return v['price'] >= price[0].value && v['price'] <= price[1].value }) render(newJson); } // 搜尋名字 btn[1].onclick = function (v) { var arr = [] // some 的效率高, 找到就馬上跳出 // 如果只找陣列中唯一元素, some 非常適合 json.some(function (v) { if (v['name'] === n.value) { arr.push(v); return true } }) render(arr); } // 一開始直接調用一次, 把所有資料顯示出來 render(json); ``` ### some 與其他人的效率差別 ```javascript= var arr = [1,2,3,4,5] arr.some(function (v) { if (v === 2){ return true } console.log(99); // 99 }) arr.forEach(function (v) { if (v === 2){ return true } console.log(99); // 四次99 }) arr.filter(function (v) { if (v === 2){ return true } console.log(99); // 四次99 }) // 只有 some 在 return 後跳出, 其他都跑完全部 ``` ### string > #### trim() > - `str.trim()` > - 去除前後空格, 返回新 str > - 中間的空格不會被刪掉 > #### 應用 ```htmlmixed= <body> <input type='text'/> <button></button> <div></div> <script src='test.js'></script> </body> ``` ```javascript= var txt = document.querySelector('input'); var btn = document.querySelector('button'); var div = document.querySelector('div'); /* 問題 btn.onclick = function () { if (txt.value) { // 如果只有空格也會進去 div.innerText = txt.value; console.log(txt.value.length); // 有時想判斷長度可能也會有困擾 } } */ btn.onclick = function () { var newStr = txt.value.trim(); if (newStr) { div.innerText = newStr; console.log(newStr.length); } } ``` ### 對象方法 > #### `Object.keys(obj)` > - 遍歷出與 for in 相同順序的陣列 ```javascript= var obj = { id: 1, name: 'GodJJ' } console.log(Object.keys(obj)); // (2) ["id", "name"] ```