WFX-Maniac

0%

關於git add的一些常識

當我git add 一個檔案之後, 我如果再去修改他, 接著再commit。
其實在這個commit,git 並不會去紀錄該檔案的改動, 因為這個檔案只是被加到暫存區而已,git沒有辦法去refer到他的歷史改動狀況。

所以建議如果要git add檔案, 一律add完之後commit一次再做改動

要commit之前一定要先add –all, 或者可以直接使用 git commit -a

想要add 一個資料夾, 包括底下的所有child folder, 可以使用git add ., 如果是想要 add 整個專案的內容,則可以用git add –all

git commit –allowed-empty -m “” 這樣寫就可以產生一個空的commit點ˊ
log

git 可以分成 工作目錄/暫存區/儲存區
用git add 把檔案從工作目錄記錄到暫存區, 用git commit 把檔案從暫存區記錄到儲存區

git log 內容有些什麼

可以看到每一個commit點

  1. commit 的人是誰?
  2. 時間點
  3. commit log

可以搭配 –oneline 或 –graph 來做不同模式的log顯示
可以用 –author 查詢特定開發者的commit內容
–author=”” 的使用, 如果開發者有兩位以上, 可以使用 userA | userB
–grep=”” 的使用, 可以尋找commit log裡面有特定字串的commit點
-S”” 的使用, 可以尋找commit 點之中,改動內容含有特定字串的commit 點
–since=”” & –until=”” & –after=”2019-01” 可以顯示特定時間點區間的commit點

git rm 是幹嘛用的? 跟一般的rm 差在哪裡?

git rm 等於 先使用一般的rm , 然後再把這個rm 的改動 用add 加到暫存區

可以使用 –cached ,這樣會變成單純的把檔案與git 斷聯,但是不會實際刪除檔案, 這樣該檔案在做完這個動作之後就會變成 untracked了

git mv 改變檔名

例如

1
2

git mv aa.html bb.html

一些簡單的基本:終端機指令

  1. 複製檔案cp
  2. 重新命名檔案 mv '舊檔名' ‘新檔名‘
  3. 列出目前位置 pwd
  4. 移除檔案 rm
  5. 到根資料夾cd ~
  6. 顯示所有包括隱藏的檔案ls -al

Vim操作

git 基本操作

  1. 使用者基本資料設定(bt device),設定姓名/電子郵件
1
2
git config --global user.name "Eddie Kao"
git config --global user.email "eddiexxxx@gmail.com"
  • 這邊拿掉global 的話就會對專案直接做個別設定
  • 存取的global使用者資料會在user根資料夾底下的.gitconfig檔案裡
    1
    ~/.gitconfig
  1. 列出當前使用者資料設定

    1
    git config --list
  2. 其實可以把git的預設編輯器改成 VSCODE
    https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git

  3. git 的操作指令其實也可以寫alias(縮寫)

1
2
git config --global alias.co checkout
//把checkout指令改成co
1
2
git config --global alias.l "log--oneline--graph"
//也可以把一些比較複雜的指令改成alias
1
2
3
git config --global alias.ls 'log --graph --pretty=format:"%h <%an> %ar %s"'
//or more complicated
//把git log的格式簡化用

專案git初始化的流程

  1. cd <folder>
  2. git init

git add --all

1
2
3
4
5
git add -A stages all changes

git add . stages new files and modifications, without deletions

git add -u stages modifications and deletions, without new files
  1. git commit -m <commit log>

TS 的class 跟 es6 有什麼差別?

1. private與 public 宣告的存在:

如果一個方法/變數/constructor被宣告為public 則能在聲明它的類的外部被取用
反之若宣告為private, 那這個方法/變數/constructor 就只能在自己的class裡面被使用

所以說這樣會錯

實際上, 當private 方法/變數/constructor 在父類別被繼承出去的時候,
他是會進到子類別(或子類別的實例)裡面的,但是就是無法訪問而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class fruit{
constructor(){

}

private type ="fruit";
method1(){
console.log(this.type);
}
}


let a = new fruit();
console.log(a.type);//報錯, 因為type 只能在fruit裡面被使用,被new 出來的實例沒辦法取得他


class apple extends fruit{
constructor(){
super();
this.type = "aa"
}
mm(){
console.log(this.type);//報錯, 因為type 只能在fruit裡面被使用,就算是extend 過來也不行
}
}

2. protect 宣告的存在:

如果一個class 的方法/變數/constructor被宣告為protected, 則:
  1. 這個方法/變數/constructor能在class裡面被叫用
    但是我們不能透過這個class的實例去拿到他被 protect 的東西
  2. 但若如果今天這個class被作為一個父類別被繼承下去的話,他的子類別在子類class的實例化前/後,
    都可以去訪問這個被protected的東西

3. readonly 宣告的存在:

如果一個class 的變數被宣告為readonly, 則:
  1. 這個方法/變數/constructor只能在class(實例化前後)被讀取,但不能被改變
  2. 就算class被作為父類別繼承,他的子類別一樣不能去修改父類別繼承下來的變數
  3. 注意 readonly只能用來把變數標注成唯讀,方法跟constructor 是不能設定的

4. getter與 setter

typescript 的 class 可以把裡面的變數(屬性) 寫成 getter與setter 的形式[color=#36c169]

如果是用VSCODE作為IDE的話,可以先把變數(屬性)反白,然後右鍵點左上角的小燈泡,這樣就可以自動產出ts 的get/set snippet 語法

以上面圖片這個例子來講, 當這個class的a屬性被改成get/set之後,就會變成下面這樣

1
2
3
4
5
6
7
8
9
10
11
12
class test{
constructor(){

}
private _a: string = "abc";
public get a(): string {
return this._a;
}
public set a(value: string) {
this._a = value;
}
}

然後當這個class被實例化之後就可以透過setter去賦值,也就是

1
2
3
let example  = new test();
test.a = "def";
// 這邊"def"會自動作為value 被帶入到set a(value)裡面去

什麼是Interface(接口)?

簡單來說就是一個用來定義一個物件內部 必要變數/變數型別的寫法
好用的點在於可以去限制一個物件必須要具備什麼樣的property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface LabeledValue {
label: string;
name?:string; //後面有問號的property代表是optional的項目
readonly color:'red'
//前面有寫readonly的property,
//代表這個項目只要被interface初始定義之後就沒有辦法再被覆寫
//注意readonly只能在interface裡面使用
}

function printLabel(labeledObj: LabeledValue
//直接把labeledObj 內容的型別定義為labeledValue 接口
) {
console.log(labeledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

什麼是ReadonlyArray(唯讀陣列)?

用途是用來確保陣列被定義為唯獨之後再也不能被修改

1
2
3
4
5
6
7
8
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
//但是可以用類型斷言強行複製
a = ro as number[];

我把函數的參數物件用一個具備optional屬性的interface來定義型別,然後我傳入不存在於該interface的property卻報錯了,為什麼?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface LabeledValue {
name?:string; //後面有問號的property代表是optional的項目
}
//所以基本上這樣寫是合法的

let qq = (bb:LabeledValue)=>{

}
qq({})//傳入空物件

// 這樣寫也是合法的
let apple:LabeledValue ={};
apple.name = 'fuji';


//但是這樣寫就會fail
apple.color = 'red';

這說明了optional屬性只能接受”存在”或“不存在”這兩種值,用一個本來就不符合interface的property是不合法的

如何使用interface 為函數定義參數型別與回傳值型別

1
2
3
4
5
6
7
interface SearchFunc {
(source: string, subString: string): boolean;
} // 定義函數參數/return 的方式

interface customVariables {
variableA: boolean;
}

如何使用interface 定義”可索引類型”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface arrayType {
[index: number]: string;
} // 意思指的是[]內只能輸入數字, 並且return 值為字串

interface readonlyArrayType {
readonly [index: number]: string;
} // 與readonly屬性的搭配應用方式, 這樣就可以把陣列轉為唯讀陣列

// 問題狀況
interface arrayType {
[index: string]: number; //當索引值的型別是字串時會有一種特殊的問題, 會強制要求內部的變數型別跟這邊定義的一樣
value:string // 所以這樣會報錯 (必須是number)
} // 當已經使用了strinf索引類型的定義時, 會強制要求回傳值是索引類型定義中的那個回傳值型別



使用implement 將class與 interface 對接

1
2
3
4
5
6
7
8
9
10
11
12
13
14

interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date(); //如果class裡面沒有寫這個變數就會報錯
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

如何快速定義一個class的constructor 參數型別與其建立出來的實例型別?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//建議直接使用表達式
interface ClockConstructor {
new (hour: number, minute: number); //定義ctor 參數內容
}

interface ClockInterface {
tick(); // 定義實例內容
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {

}
}

interface也可以extend

實際使用起來就跟class的extend 差不多

1
2
3
4
5
6
7
8
9
10
11
12
13

interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = <Square>{};
// 這邊也可以寫成 let square:Square = {color:'blue',sideLength=10}, 但如果不寫預設值就會報錯
square.color = "blue";
square.sideLength = 10;

interface extend class? 用意是什麼?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void; // 這邊定義了一個extend了 control的接口, 所以意思是說,如果今天有class想要implement 這個接口,他必須要有control 的全內容, 不然就會報錯
}

class Button extends Control implements SelectableControl {
select() { } // 因為button 有 extend Control, 所以他本身就具有private 的state, 所以這樣給過

class TextBox extends Control {
select() { }
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
// 因為Image 本身沒有private 的state, 所以不能implement SelectableControl
}


TYPESCRIPT的基礎常識

基礎型別上與es5/es6 較有差異的部分

  1. 數字
    ts的數字另外有支援兩種格式:2進位&8進位;除此之外,ts 的number同樣都是浮點數

    1
    2
    3
    4
    let decLiteral: number = 6;
    let hexLiteral: number = 0xf00d;
    let binaryLiteral: number = 0b1010;
    let octalLiteral: number = 0o744;
  2. 陣列

    1
    2
    3
    //原則上ts的變數都要寫型別,而如果一個變數的型別是array
    //且這個array的子項型別都是number,則可寫成如下
    let list: number[] = [1, 2, 3];
1
2
//承上,或如下利用泛型
let list: number[] = [1, 2, 3];
  1. 元組(陣列型別專用)
  • 最基本的範例:

    1
    2
    3
    4
    5
    6
    // Declare a tuple type
    let x: [string, number];
    // Initialize it
    x = ['hello', 10]; // OK
    // Initialize it incorrectly
    x = [10, 'hello']; // Error
  • 為陣列編寫型別的好處

1
2
3
4
let x: [string, number];
x = ['hello', 10]; // OK
console.log(x[0].substr(1)); // OK 因為x[0]在元組中定義為字串,可以使用substr方法
console.log(x[1].substr(1)); // Error, 因為10屬於在元組中定義為數字,不能使用substr方法,所以會報錯
  • 當偵測到陣列子項位置在元組定義範圍外的子項時,會用元組定義的型別的聯集來定義之
1
2
3
4
5
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型
  1. 列舉(Enumerate)
  • 能夠把變數的範圍限制在某些限制下進行存取並賦予其定義

  • 舉個實用的例子,團隊裡的後端在 Server 處理資料發生錯誤時,定義了一些代碼,來告訴前端說,這個代碼代表什麼錯誤,那個又是什麼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleWrongStatus = (status: string): void => {
switch (status) {
case 'A':
// Do something...
break;
case 'B':
// Do something...
break;
case 'C':
// Do something...
break;
default:
throw (new Error('No have wrong code!'));
}
};

然而像上面這樣寫的話,缺點會在於各個case之間沒有語意化,你很難去釐清Case A 實際上是什麼, Case B又是什麼,在這種狀況下就可以去使用enumerate

enumerate的寫法跟js的物件接近,如下

1
2
3
4
enum requestStatusCodes {
error,
success,
}

正常來講,如果不幫上述這個requestStatusCodes 的裡面每一個子項去做定義,TS的編譯器會直接把error的編號定為0, success的編號定為1(流水號)

這樣我們就可以透過requestStatusCodes.error拿到0這個列舉值, requestStatusCodes.success拿到1這個列舉值;或者透過requestStatusCodes[0]拿到error 這個列舉子項

這樣的話。上述的case switch就可以這樣改良

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum requestWrongCodes {
missingParameter = 'A',
wrongParameterType = 'B',
invalidToken = 'C',
}

const handleWrongStatus = (status: string): void => {
//es6解構賦值的寫法
const { missingParameter, wrongParameterType, invalidToken, } = requestWrongCodes;
switch (status) {
case missingParameter:
// Do something...
break;
case wrongParameterType:
// Do something...
break;
case invalidToken:
// Do something...
break;
default:
throw (new Error('No have wrong code!'));
}
};

5.any/void 任意值與虛空值

變數的型別如果不確定的話可以直接帶any,而函數的return type如果為空的話則可以直接帶:void

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead"; //這樣不會報錯
notSure = false; //這樣也不會報錯
1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

6.undefined/null 未定義與空值

TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和void相似,它们的本身的类型用处不是很大

1
2
3
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

在默認狀況下 undefined和null 這兩種value也可以被賦予給任何型別作為其值

1
2
let u: number = undefined; 
let n: syting = null;
  • –strictNullChecks是什麼?

    簡單來說就是typescript 的tsconfig.json裡面的一個設定,只要做了這個設定以後,undefined和null 這兩種value就再也不會被默認為任何型別的一種預設值。也許在某處你想嘗試一個字串或空或未定義,可以使用聯合類型 ex: string|number|null

1
2
3
4
5
6
7
8
9
{
"compileOnSave": false,
"compilerOptions": {
...
"strictNullChecks": true,
"skipLibCheck": true,
...
}
}
  1. Never 這個型別是用來幹嘛用的?

never類型表示的是那些根本就不會有值的類型,例如一個只會return new Error(‘string’) 的function;
要注意值為never型別不接受null作為其值。

1
2
3
function error(message: string): never {
throw new Error(message);
}
1
2
let something: void = null;
let nothing: never = null; // Error: Type 'null' is not assignable to type 'never'
  1. 類型斷言

類型斷言好比其它語言裡的類型轉換,但是不進行特殊的數據檢查和解構。
.jsx檔案裡面只能用as的方式做類型斷言

1
2
3
let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
1
2
3
let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

1. 實作雙/單向 prop trap綁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const input = document.querySelector('#username')
const handler = {
set: function(target, key, value) {
if (target.id && key === 'username') {
target[key] = value;
document.querySelector(`#${target.id}`)
.value = value;
return true
}
return false
}
}

const proxy = new Proxy(inputState, handler)
proxy.value = 'John Doe'

2. trap呼叫無效/ 不存在方法的行為並throw error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Foo() {
return new Proxy(this, {
get: function (object, property) {
if (Reflect.has(object, property)) {
return Reflect.get(object, property);
} else {
return function methodMissing() {
console.log('you called ' + property + ' but it doesn\'t exist!');
}
}
}
});
}

Foo.prototype.bar = function () {
console.log('you called bar. Good job!');
}

foo = new Foo();
foo.bar();
//=> you called bar. Good job!
foo.this_method_does_not_exist()
//=> you called this_method_does_not_exist but it doesn't exist

什麼是Proxy

字面上的意思就是”代理” [color=red]

  • Q1: 所以說是要代理些什麼東西啊?
  • A1: 所謂的代理, 就是在要去代理一個物件內部 的 <屬性的存取>。過程中Proxy的作用方式,
    有點像是去形成一個攔截屏障,所有要存取目標物件的程序都需要去通過這層屏障才能完成存取,所以說這層屏障的用途就在於偵測屬性的被存取

這邊是一個利用Proxy去為一個空物件撰寫getter/setter的範例

1
2
3
4
5
6
7
8
9
10
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});

Proxy 的基本語法

1
let p = new Proxy(target, handler);
  • 其中 target 是一個目標的物件(雖然說是物件,但是它實際上也可以是array/function,甚至是另外一個proxy)

  • handler 也是一個物件,但是他裡面的屬性 是proxy攔截(trap)到屬性存取行為時可以使用的函數。

  • 基本上,我們在定義handler的時候並不是直接給一個有很多自訂名稱的方法的物件。

  • handler裡面可以定義的方法有下面幾種(共13種):

    • **get(target, propKey, receiver)**:攔截對象屬性的讀取,比如proxy.foo和proxy[‘foo’]。
    • **set(target, propKey, value, receiver)**:攔截對象屬性的設置,比如proxy.foo = v或proxy[‘foo’] = v,返回一個Boolean。
    • **has(target, propKey)**:攔截propKey in proxy的操作,返回一個Boolean。
    • **deleteProperty(target, propKey)**:攔截delete proxy[propKey]的操作,返回一個Boolean。
    • **ownKeys(target)**:攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
    • **getOwnPropertyDescriptor(target, propKey)**:攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
    • **defineProperty(target, propKey, propDesc)**:攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個Boolean。
    • **preventExtensions(target)**:攔截Object.preventExtensions(proxy),返回一個Boolean。
    • **getPrototypeOf(target)**:攔截Object.getPrototypeOf(proxy),返回一個對象。
    • **isExtensible(target)**:攔截Object.isExtensible(proxy),返回一個Boolean。
    • **setPrototypeOf(target, proto)**:攔截Object.setPrototypeOf(proxy, proto),返回一個Boolean。如果目標對像是函數,那麼還有兩種額外操作可以攔截。
    • **apply(target, object, args)**:攔截Proxy 實例作為函數調用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(.. .)。
    • **construct(target, args)**:攔截 Proxy 實例作為構造函數調用的操作,比如new proxy(…args)。

MDN上面有更完整的說明 [color=red]
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy#Handler_functions

先來看個 Proxy 的基本演練

source code from MDN [color=red]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let handler = {
get: function(target, name) {
return name in target ?
target[name] :
37;
},
set: function(target, name, value) {
if (name === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}

// The default behavior to store the value
target[name] = value;

// Indicate success
return true;
}
};

let p = new Proxy({}, handler);

這樣寫的話,當我們去執行console.log(p.abc),也就是去取用p的屬性值,就會觸發get function
而如果我們去執行p.abc = “123”, 就會觸發set function,這就是最基本的handler.get和handler.set的運用

下週繼續: 常用的handler方法與他們的使用範例

1. handler.apply

基本上大家都知道apply就跟call 是類似的東西, 他們是被用來執行一個函數, 並且可以塞特定的變數(或array)進去函式作為 this 使用;
而當一個函式被帶入proxy建構式而建立起proxy物件的時候, 因為函式本身也是一種物件(見MDN解說, 所以這樣就會變成只要該函式被以任何一種形式(一般/call/apply)呼叫的時候就會觸發代理的apply

基本的型態是這樣(如下)

1
2
3

apply: function(target, thisArg, argumentsList)

這邊target 是被代理的函式, thisArg 則是當這個被代理的函數是被call或者apply所呼叫時填入的this , argumentsList則是這個函數在被呼叫時的參數群

2. handler.new

這應該也是蠻好理解的, 簡單來說就是當一個能被作為建構子使用的函數, 被以proxy代理, 這樣的話, 他在被new 產生實例的時候, 就會觸發construct;

基本的型態是這樣(如下)

1
2
3

contruct: function(target, args)

3. handler.defineProperty

這個就有點意思了, handler.defineProperty 是proxy 用來代理 object.defineProperty的對應handler形式, 那什麼是object.defineProperty呢?
簡單來講, object.defineProperty就是去賦值給一個物件的prop, 或者是產生並賦值一個prop。
這聽起來似乎很像是set 或是 object[‘prop’]=xxx 這樣的操作, 而的確object[‘prop’]=xxx的背後就是用object.defineProperty做成的。

他正確的形式是像這樣寫的

1
2
Object.defineProperty(obj, prop, descriptor) 

這邊的obj就是 想要去賦值prop的物件,prop就是想要去改變的property, 而descriptor則是一個物件, 裡面預設包含:

1
2
3
4
5
6
7
8
{
enumerable: false,
configurable: false,
writable: false,
value: 'value',
get:()=>{}
set:()=>{}
}

我們平常使用obj[‘prop’]=value, 其實等同於是:

1
2
3
4
5
6
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
writable: true,
value: 'value'
})

這邊enumerable的用途是決定是否放行Object.keys() 或 for…in loop 去遍歷到這個屬性
configurable代表的是這次操作之後能不能再重新用Object.defineProperty去改寫這個prop的descriptor
而writable則是決定這個prop可不可以被覆寫
get/set 基本上就是一般理解的get/set 函數。

之後有機會的話再來寫一篇心得探討Object.defineProperty的用途

那讓我們回歸到正題。

我們這邊講到的是用proxy 去代理物件時,利用handler 中的 defineProperty trap 去偵測defineProperty行為, 這應該就不難理解了
基本上只要是透過Object.defineProperty所做出來的操作, 全部都會觸發這個trap

所以,

1
2
3
4
5
6
7
8
9
10
11
obj['prop']=value //會觸發

Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
writable: true,
value: 'value'
}) //會觸發

let a = Object.prop //當然也會觸發

handler.defineProperty 的基本形式如下

1
defineProperty: function(target, property, descriptor)

唯一一個比較需要注意的點是, handler.defineProperty 必須要return 一個 boolean值(用來確定prop是否被正確define), 不然會報錯

4. handler.set

set 基本上就跟一般認知的set 是一樣的東西, 簡單來說就是去設定物件prop的值, 和handler.defineProperty的差別在於, 用set 沒有辦法trap到利用Object.defineProperty 所做的操作

基本的set 形式為:

1
set: function(target, property, value, receiver) {}

這邊的target 就是被代理的物件
property則是想要被set 值的prop
value 是想要給予prop的值
receiver 這個比較 特別, 這個預設都是指向proxy 實例自己, 但是在某些狀況底下也可以是別的東西(這個後面再討論)

4. handler.get

跟handler.get 同理

get 的形式是

1
get: function(target, property, receiver) {}

和.set一樣, 最後必須要return true

什麼是Reflect 和 receiver?

Reflect

Reflect 實際上和Proxy 是互相獨立的feature,但是 Reflect 常常跟著 Proxy一起被使用
Reflect本身是一種 built-in Object (跟Math一樣), 裡面的method就跟proxy的預設 handler集一樣(名稱一樣, 但是各個函數內容稍微有點差異)

什麼時候會用到Reflect

平常沒有用到proxy的時候(讓對物件的操作可以變成用refect的method來處理, 優點在於統一整體code的一致性)

1
2
'_secretDrink' in FooBar;
delete FooBar._secretDrink;

有了 Reflect 我們可以這樣做:

1
2
3
Reflect.has(FooBar, '_secretDrink');
Reflect.deleteProperty(FooBar, '_secretDrink');

當有用到proxy的時候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

const target = {
get foo() {
return this.bar;
},
bar: 3
};
const handler = {
get(target, propertyKey, receiver) {
if (propertyKey === 'bar') return 2;// 這邊透過reflect.get 將可以拿到handler.get 在特定條件下丟出來的值, 但是如果是直接透過target用特定key取值, 則會直接取用原生物件(非代理)的get func, 導致最後的到的值變成3
console.log('Reflect.get ', Reflect.get(target, propertyKey, receiver)); // this in foo getter references Proxy instance; logs 2
console.log('target[propertyKey] ', target[propertyKey]); // this in foo getter references "target" - logs 3
}
};
const obj = new Proxy(target, handler); //
console.log(obj.bar); // 因為 obj是一個proxy實例, 所以取用.bar會觸發 handler.get
// 2
obj.foo;
// Reflect.get 2
// target[propertyKey] 3

從這個案例來看, 我們可以發現refect的用意就是要去取得”代理” 這一個目標, 確保可以使用到代理內部handler的get, 而不是去使用原生物件的 getter

receiver

什麼是receiver?

一般狀況下 receiver會去指向代理本身,比方說

1
get: function(target, property, receiver) {}

通常來講這個receiver 就會是指向使用了這個handler的代理自己, 但是有些狀況下並不是這樣的。

這邊不太好懂, 可以看這篇stackoverflow

https://stackoverflow.com/questions/37563495/what-is-a-receiver-in-javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var handlers = {
get(target,key,context) {
console.log(greeter === context); //true, this line added
return function() {
context.speak(key + "!");
};
}
},
catchall = new Proxy( {}, handlers ),
greeter = {
speak(who = "someone") {
console.log( "hello", who );
}
};

// setup `greeter` to fall back to `catchall`
Object.setPrototypeOf( greeter, catchall );

greeter.speak(); // hello someone 發動greeter的speak方法, 很正常的拿到了 預設的 someone
greeter.speak( "world" ); // hello world 給予 world 這個字串參數, 也很正常的拿到了 hello world

greeter.everyone(); // hello everyone!


這邊的greeter.everyone是一個根本不存在的方法, 但是這邊被call了
這樣最後會跑去觸發到由setPropertyOf 繼承到的catchall(是一個proxy實例)的handler.get
在這種狀況下 get() 的receiver 參數就不會去指向 catchall, 而是指向greeter

Proxy 實用的地方

1. 實作單向 prop trap綁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const inputState = {
id: 'username',
value: ''
}
const input = document.querySelector('#username')
const handler = {
set: function(target, key, value) {
if (target.id && key === 'username') {
target[key] = value;
document.querySelector(`#${target.id}`)
.value = value;
return true
}
return false
}
}

const proxy = new Proxy(inputState, handler)
proxy.value = 'John Doe'

// 因為真的很基本, 就不特別解釋了

2. trap呼叫無效/ 不存在方法的行為並throw error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Foo() {
return new Proxy(this, {
get: function (object, property) {
// 用reflect.has 去判斷 object 是不是有那個key 值
if (Reflect.has(object, property)) {
return Reflect.get(object, property);
} else {
//如果方法不存在, 那就return 一個攔截用的function
return function methodMissing() {
console.log('you called ' + property + ' but it doesn\'t exist!');
}
}
}
});
}

Foo.prototype.bar = function () {
console.log('you called bar. Good job!');
}

foo = new Foo();
foo.bar();
//=> you called bar. Good job!
foo.this_method_does_not_exist()
// error 被攔截
//=> you called this_method_does_not_exist but it doesn't exist

實作 interpolation

這個比較複雜,預計下週在補完

ES6 Promise 重點整理

  1. Promise 是什麼
  • 用來處理callback chain 的一種新JS語法
  • 本身是一個class, 但是他也有自己的靜態方法
  • 建立出來的實例物件具有三種狀態:pending/fulfilled/rejected
  • 狀態只可能是從pending 變成 fulfilled/rejected
  1. promise.prototype.then()
  • then()可以傳入兩個函數作為參數,第一個函數會在promise物件被fulfilled之後馬上發動,第二個函數則會在promise物件被reject的時候馬上發動
  • 會回傳一個新的promise物件
  • 重要 : 當then與then串接的時候,後面的then 的 參數function 會得到前面的then 的 參數function return出來的值作為參數
  1. promise.prototype.catch()
  • catch()可以傳入一個函數作為參數,並且這個函數會接收到promise throw出來的error
  • 也會回傳一個新的promise物件
  1. promise.all()
  • 也會回傳新的promise
  • 必須要傳入一個陣列,這個陣列本身的內容物都是promise
  • 當這些內容物promise都被fullfill之後,all()本身回傳的promise就會被fullfill
  • 在all() 回傳得到的promise後面去使用then(), 該 then()的參數function會拿到all()的所有內容promise 在resolve 時使用的參數 所組成的陣列。
  1. promise.race()
  • 也會回傳新的promise
  • 必須要傳入一個陣列,這個陣列本身的內容物都是promise
  • 當這些內容物promise有其中一個被fullfill之後,race()本身回傳的promise就會被fullfill
  • 在race() 回傳得到的promise後面去使用then(), 該 then()的參數function會拿到race()的最快達成fullfilled 的內容promise 在resolve 時使用的參數。