4. JavaScript 環境與規則原理 - ZoeHYH/mentor-program-4th GitHub Wiki

JavaScript 介紹與環境建置

過去 JavaScript 只能在瀏覽器上運行,在開發人員工具的 console 裡輸入程式碼。

使用 Node.js 可以在電腦運行 JavaScript 。

語言也會進行更新,設定新的語法與型別,目前 JavaScript 更新到 ES6 ,更新後還要一段時間,瀏覽器才會跟上,語法才能普及。

在瀏覽器執行 HTML 檔案

vim 檔案名稱.html #儲存即可直接建立檔案
<script>
debuuger #啟用開發人員工具 source 頁面 debug 功能
#內容
</script>
open 檔案
#在開發人員工具裡檢視

在 CLI 執行 JavaScript

#下載 installer ,並安裝程式
node -v #在 CLI 輸入,顯示當前版本表示安裝成功
vim 檔案名稱.js #直接輸入 JavaScript 
node 檔案 #在 CLI 印出結果
node # Enter 進入JavaScript 環境
#輸入 JavaScript 
# Ctrl+c 跳出
process.argv #放在 JavaScript 裡可印出 node 運行時傳入的參數
process.argv[0] # node 路徑
process.argv[1] # JavaScript 檔案路徑
process.argv[其他數字] #後續參數

輸入外部資料

Online Judge 系統 與 CLI 手動輸入資料

  • JavaScript 程式撰寫

    var readline = require('readline'); //執行後手動輸入資料
    var lines = []
    var rl = readline.createInterface({
      input: process.stdin //使用文件就會直接輸入到 input
    });
    rl.on('line', function (line) {
      lines.push(line) //將每行內容存到陣列
    });
    rl.on('close', function() {
      solve(lines) //關閉前輸出
    })
    function solve(lines) {
    	//撰寫 JavaScript 函式
    }
    var readline = require('readline'); //執行後手動輸入資料
    var lines = []
    var rl = readline.createInterface({
      input: process.stdin //使用文件就會直接輸入到 input
    });
    rl.on('line', function (line) {
      lines.push(line) //將每行內容存到陣列
    });
    rl.on('close', function() {
      aolve(lines) //關閉前輸出
    })
    function solve(lines) {
    	//撰寫 JavaScript 函式
    }
  • CLI

    node 檔案
    #輸入
    # Windows Crtl+z 結束輸入,其他 Crtl+d

使用其他檔案的資料

cat input.txt | env node code.js
cat input.txt | command node code.js
cat input.txt | node code.js # windows 可能無法使用

基礎

console.log('字串') //印出字串,同時也是重要的 debug 工具
//註解一行
/*註解多行*/

陳述句 Statement

執行動作, ifswitchfor等等。

if (a === 3) { // if 本身是 Statement
  console.log('Hello');
}

表達式 Expression

輸入後會回傳值的陳述句,可被簡化為某種值,最基本的就是用變數與運算子構成。

2 + 4

變數

一個放資料位置的箱子。

靜態作用域 Static Scope

調用變數時,電腦會一層一層往外尋找變數宣告的作用域,相對於動態作用域 Dynamic Scope ,靜態作用域在宣告時就固定了。

作用域鏈 Scope Chain

一個物件清單。

進入function會產生一個EC,儲存 AO, Scope Chain 也會被建立並初始化,傳入global的 VO 或上層function的 VO 。

//作用域模擬
函式EC: {
	AO:{
		變數
	},
	scopeChain: [函式EC.AO, global[[Scope]]]
	= [函式EC.AO, globalEC.VO] //變數溯源的過程
}

globalEC: {
	VO: {
		變數
	},
	scopeChain: [globalEC.VO]
}

函式.[[Scope]] = globalEC.scopeChain //函式被呼叫
  • Execution Context :global具有 EC,每當進入一個函式也會產生一個 EC 。儲存 VO 、 AO 與傳入的 Scope Chain 等相關資訊,並放到 stack 裡,函式執行完畢就會刪除回傳。
  • Variable Object :每個 EC 都有對應的 VO ,儲存變數與函式,以及函式的參數。global VO 存有全域變數。
  • Activation Object :VO 的特別型態,在函式的 EC 中建立,並當成 VO 使用,存有參數、變數,arguments也存這裡。

最小權限原則 Principle of Least Privilege / 授權 Authority / 暴露 Exposure:避免「汙染」!

function func(a) {
  i = 3;
}
for (var i = 0; i < 10; i++) { 
  b(i * 2); // 6 , i 在函式裡被重新賦值了
}

全域global

在全域命名空間 Global Namespace 裡宣告,任何地方都能用。

a = 1 //在非嚴格模式下直接賦值等於建立全域變數

實際上,全域變數不是變數,而是全域物件(底層物件)window, node 環境稱global,的屬性。

console.log(window.a) // 1

函式function

在函式裡宣告,只在函式裡能用。

區塊block-level

只在大括弧{}的範圍內有效,解決迴圈內的變數會被誤改的問題,只有letconst可以宣告。

宣告語法 Assign

分配記憶體空間。

var 變數名稱 =  //宣告並賦值
var a // 宣告未賦值
a // undefined
b // 錯誤: not defined 

命名應統一且語意化

var firstVar //駝峰式命名 Camel Case
var second_var //底線命名 Underscore
// Case Sensitive:大小寫有差
var var //不可使用保留字
var 1var //不能用數字開頭
var %^ //特殊符號有些也不行

隱性型別宣告

  • var:視所在位置建立全域函式作用域,可重新宣告、賦值,不再建議使用。
  • const:建立區塊作用域,不可重新宣告,可重新賦值,用於宣告常數與物件,在 JavaScript 中所宣告物件仍是可變的。
  • let:建立區塊作用域,宣告時必須賦值,不可重新宣告與賦值,用於for迴圈或需要再指定值的情況,它的特性會讓迴圈每跑一次就產生新的作用域。

解構 Destructing

let [a, b] = [1, 2] //可以傳值或傳變數
const obj = { 
   key1: '1',
   key2: 2,
}
let {key1, key2} = obj //變數名稱必須對應 key
key1 // '1'
let [a, ...b] = [1, 2, 3] //搭配展開運算子

預設值 Default Parameters

let [a, b, c=3] = b
function sum(a, b = 2) {}
sum(1) //設了預設值,只傳入一個值也可行

宣告變數時不需要宣告型別,但執行跨型別的運算時會自動轉換型別

console.log(數字+字串) //字串
  • 原始型別或都傳值 pass by value:當宣告新變數並賦予原本變數的值,電腦傳遞的是新增的拷貝位置。
  • 物件型別傳址 pass by reference :當宣告新變數並賦值為原本變數,電腦傳遞的是原本變數的記憶體位置。
  • 還是物件型別或都 pass by sharing ? 又或者傳遞的是原本的位置,只是記憶體內容不可改變。

引用

LHS 引用 Left hand side

查詢變數位置,例如對變數賦值時。

RHS 引用 Right hand side

查詢變數值,例如呼叫變數時。

提升 Hoisting

宣告變數的程式碼會先被編譯器讀取,寫入 EC 裡的 VO 或 AO。所以可在宣告前就使用變數。

具編譯器的直譯型語言

編譯是把原始碼編譯成目的碼,並保證兩者執行的結果相同;直譯則是直接執行原始碼的語義,並輸出結果,但做法是個黑盒子。

順序

  1. function最優先,連同賦值也就是內容也會一起寫入。所以var的宣告會被忽略,但變數仍可在逐行執行時被覆寫。
  2. 接著是function傳入的引數 Argument
  3. var最後,且宣告變數的值是undefined

Temporal Dead Zone

由於letconst的宣告會被提升,但不會被初始化為undefined,在提升後與賦值前存取變數會拋出錯誤,這段時間被稱為 TDZ。

閉包 Closure

由函式和與其相關的參照環境組合而成的實體。由於回傳的函式的 Scope Chain 會存取上層函式的 AO,因此即使上層函式的資料已從記憶體釋放,也可以藉由回傳的函式讀取。

function a() {
	return function b() {
		//如果用到了 a 的變數,該變數就會被存進 b 的 EC
	}
**}**
//兩種 return 選一種
function a() {
	function b() {
	}
	return b 
}

letconst還沒出現時,迴圈的var必須包在function裡避免被存取修改。

for(var i=0; i<n; i++) {
  function() {
    函式(i) //執行時迴圈已經跑完了
  })
}
//使用閉包
function 回傳函式(參數) {
  return function() {
    函式(參數)
  }
}
for(var i=0; i<n; i++) {
  回傳函式(i) //每一圈都回傳函式(i)
}
//用 IIFE(Immediately Invoked Function Expression)包起函式
for(var i=0; i<n; i++) {
  (function(參數) {
    function(i) {
      函式(i)
    })
  })(i)
}

儲存函式運算的結果

function a(num) {
	//複雜運算
	return num
}
function b(func) {
	var ans = {} //記起值
	return function (num) { //直接回傳函式結果
		if (ans[num]) return ans[num]
		ans[num] = func(num)
	}
	return ans[num]
}
const c = b(a)
console.log(c(20)) //在 b 裡呼叫 a 運算
console.log(c(20)) //在 b 裡呼叫出上一次的結果

物件導向

類別class 與 instance - 類別裡的元素

ES6 才出現的語法糖 Syntactic sugar (對語言功能沒有影響,但更方便設計而發明的語法),讓模擬物件導向的過程更簡潔可讀。

class 類別名稱 { //類別應首字大寫
	constructor {
		this.參數 = 參數 //儲存建構時傳入的引數
	}
	名稱(參數) { //初始化設定用 setter
		this.參數 = 參數 // this 指的是剛剛呼叫函式的變數,用 this.參數儲存引數
	}
	名稱() {
		//撰寫要做的事
	}
	名稱() { //為這個類別設置可用方法
		console.log(this.參數) //呼叫函式就會印出參數
	}
	名稱() {
		return 參數 //呼叫方法就回傳參數
	}
}
var 變數名稱 = new 類別()
var 變數名稱 = new 類別(引數)
變數.初始化(引數) //初始化設定
變數.方法

this

值只關乎呼叫時的this究竟代表什麼,與作用域和程式碼位置完全無關。

a.b.c.hello.call(a.b.c) //觀察 a.b.c.hello() this 值的方法
  • 在 class 中代表呼叫這個方法的變數
  • 在針對瀏覽器操作的函式中,指的是操作的對象
  • node.js 預設為 global
  • 瀏覽器預設為window
  • 在嚴格模式會變成undefined,用'use strict'放在程式開頭開啟
  • 呼叫在箭頭函式作用域裡宣告的函式,this就是箭頭函式的this

更改

  • 'use strict':非嚴格模式下,傳入的this值會轉成物件
函式.call(this的值, 參數, 參數) //印出
函式.apply(this的值, [參數的陣列])
const 新函式 = 原函式.bind(this的值) //一旦 bind , call apply 就無用了

在 ES5 建立類別的方法

在 ES5 時,透過建立function設計類似class的機制。

function 類別名稱(參數) { //建構式 constructor
  this.參數 = 參數;
  this.方法 = function () { //功能
		//但每新增一個 instance 就會佔據多一份記憶體位置
  }
}

類別.prototype.方法 = function () {
	//這樣就可以共用這個功能,只佔據一份記憶體位置
}

Array.prototype.方法 = function () {
	//這樣會在陣列這個類別上新增功能,但不建議
}

var instance = new 類別(引數)
instance.方法()

原型鏈 Prototype Chain

原型鏈可以做到類似繼承的效果,電腦透過原型鏈尋找所呼叫方法的函式,從 instance 本身的原型prototype找到類別.prototype,找不到就會繼續去找物件型別的原型,直到找到null都沒有。

  • prototype

    Object.getPrototypeOf(instance) //回傳 instance 的 prototype
    .__proto__ //可用但比較不推薦,在這裡用來解說
    instance.__proto__ === 類別.prototype
    類別.prototype.__proto__ === Function.prototype //類別就是函式類別的 instance
    Function.prototype.__proto__ === Object.prototype
    Object.prototype.__proto__ //指向 null
    原型.hasOwnProperty('方法') //原型是否存有這個方法
  • instance

    a instanceof b //可以判斷兩者關係
    Function instanceof Object === Object instanceof Function //互為 instance
    Function.__proto__ === Function.prototype // true
    Function.__proto__.__proto__ === Object.prototype // true
  • constructor :每個 prototype 都有的屬性,指向構造函數

    instance.constructor === 類別 // true
    任一類別.prototype.constructor === 任一類別 // true
    類別.prototype.hasOwnProperty('constructor') // true
  • new:創造新的物件,把物件的__proto__指向類別的prototype,

包 Package / 模組 Module

所謂模組就是各式各樣具特定功能的程式,由不同開發者寫好並分享到網路上,供需要這些功能的人下載引用,省去每個人都要開發同一個功能的浪費。一個模組可以是一個資料夾裡放著幾個檔案,同時包含 package.json 。

引入輸出

ES5 - requireexport

var 模組 = require('模組名稱')
var 我的模組 = require('./模組.js') //也可只傳模組讓它搜尋
模組.方法() //使用模組裡的方法
module.exports = 輸出資料 //可以輸出任何東西

ES6 - importexport

  • 安裝 Babel
    • npm install — save-dev @babel/core @babel/node @babel/preset-env

    • 資料夾裡新增.babelrc檔案

    • 檔案裡新增

      {
         "presets": ["@babel/preset-env"]
      }
    • npx babel-node就如同node模式,正常運作表示成功

  • 普通模組
    • 添加在在宣告語法前export function等宣告語法 模組名稱或是export {模組, 模組}另外放置
    • import {模組, 模組} from '檔案路徑'
  • default預設集
    • export default function等宣告語法
    • import default的模組, { 模組 }import { default as 預設集名稱, 模組}
  • *所有模組:import * as 名稱 from 'export 的檔案路徑'
  • 別名語法as
    • export {模組 as 別名, 模組}
    • import {模組 as 別名, 模組} from 'export 的檔案路徑
  • CLI 執行:npx babel-node import 的檔案名稱

模組管理器

Yarn Package Manage

ypm 是 npm 的替代選項,由 Facebook 主導開發,更快且安全,同時兼容 npm 。

npm 是其中一種供人上傳分享並下載各式 JavaScript 模組的公共平台,方便我們利用別人的模組。

npm 依附於 node.js 下,會一起安裝好,但更新頻率較高,可隨時下載最新版。

  • 安裝 npm

    npm -v #可以檢視安裝的版本
    npm install npm@latest -g #也拿來可以下載其他模組
  • 安裝專案使用的套件

    npm install #下載別人專案用到的所有模組
    npm install --production #只下載使用別人專案會用到的模組, dependencies
    npm install 模組名稱 #建立 node_modules 目錄,將指定模組載進去
    npm i 模組名稱 #縮寫
    #如果專案中沒有 package.json 就會下載最新版本
    npm install 模組名稱@版本 #指定安裝版本
    npm update 模組 #查詢遠端最新版本,比對本地,檢視 package.json 中的語意版本規則決定是否下載
    npm outdated #回傳目前本地版本、 package.json 宣告版本、遠端最新版本
  • 安裝要在 CLI 介面使用的套件

    npm install -g 模組名稱 #全域性安裝模組
    npm ls -g --depth=0 #檢視全域的模組
    npm update -g 模組
    npm outdated -g --depth=0
    npm uninstall -g <package>

packege.json

管理安裝的 npm 模組最好的方式就是建立 packege.json 檔案。

npm init #在當前目錄建立檔案,回答問題或使用預設後輸入 yes
npm init --yes #省略回答步驟
npm set init.key名稱 "內容" # author.email、author.name、license
npm install 模組名稱 --save #新增模組到 dependencies
npm install 模組名稱 --save-dev #新增模組到 devDependencies
  • 描述你的專案依賴哪些模組
  • 允許使用語意化版本規則指明模組版本
  • 更易分享、重複使用模組
{ //前兩項是基本要有的
"name": "模組名稱", //全小寫、無空格、可用橫線或下劃線
"version": "數字.數字.數字", //符合“語義化版本規則”
"description": "描述資訊", //幫助搜尋,沒寫會使用 README.md 第一行
"main": 入口檔案, //一般都是 index.js
"scripts": {"代號": "指令", "test": "為預設且指令為空", "start": "開啟專案"},
"keywords": "關鍵字", //幫助 npm search 的搜尋
"author": {作者資訊},
"license": "MIT", //預設
"bugs": "當前專案的錯誤資訊",
"dependencies": {"模組": "^版本"}, //使用時依賴的模組,供使用專案者下載
"devDependencies": {"模組": "^版本"} //開發測試時依賴的模組
}

語義化版本規則 Semantic versioning

npm 制定的版本號規則。

大版本號.小版本號.補丁版本號 //提供者會更新不同數字,標明升級幅度
大版本.小版本 大版本.小版本.x ~大版本.小版本.補丁版本
1.0 1.0.x ~1.0.4 //只接受補丁版本的更新
大版本 大版本.小版本 ^大版本.小版本.補丁版本
1 1.x ^1.0.4 //接受小版本更新
* x //接受大版本更新

npm script

呼叫 package.json 裡的 script ,直接執行代碼,例如安裝特定套件或執行專案。

{
	"script": {"start": "node index.js"}
}
npm run start

測試

Jest

由 Facebook 開發的套件,專門測試 JavaScript 程式碼的框架,可用 npm 或 ypm 安裝。

單元測試 Unit Testing

針對function輸出入做測試。

module.export = 被測物 //在測試檔案輸出
//新增測試用檔案 test.js
const 被測物 = require('./被測物') //引入
describe('描述這個區塊測試', () => { //說明 test 測試同一段程式碼,非必要,但輸出後會有 sum 測試的總結
	test('描述測試', () => { //測試不同輸入輸出結果
		expect(被測物(引數)).toBe(預期輸出結果);
	})
})
  • 使用做好的測試檔
    • 使用 package.json 的"scripts",在 CLI 輸入npm run test
      • "test" : "jest"
      • "test" : "jest test.js"
    • 新版 npm 可以直接輸入npx jest test.js

測試驅動開發 Test-Driven Development TDD

先建立test()的架構再開發,在 debug 幫助很大。

⚠️ **GitHub.com Fallback** ⚠️