Giter Club home page Giter Club logo

blog's Introduction

後續已搬遷 Blog 至 https://blog.hubertyang.com/

緣起

從 2016 年末開始工作,約莫過了半年意識到小腦袋其實記不住這麼多知識,於是開始把自己遇到的問題做成筆記,雖然說是筆記,但其實因為沒整理觀看時還蠻雜亂的,然後最近看到蠻多大神把文章放在 GitHub 上,心想我是不是也能系統性地整理自己的筆記,也練習把自己學到的知識運用文字表達出來,願我能堅持啊 XD

文章列表

blog's People

Contributors

marshal604 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

jasonccliu

blog's Issues

You Don't Know JS 小筆記 - this & prototype

參考文章 You Don't Know JS: this & Object Prototypes

關於this

什麼是this

this是在執行時綁定,而不像詞法作用域,在定義時就已經決定的。

如何判斷this綁定的位置

有四個方法可以判斷this

  • 使用 new 建構子時,會強制將this指向被new的函式
  • 使用bind, call, apply可以將this強制轉為輸入的參數
  • 呼叫時有context object的則以context object的環境為this
  • 沒有上述條件的,默認的this通常為global, 若為嚴格模式,則thisundefined

使用 new 建構子時,會強制將this指向被new的函式

function fn() {
  console.log(this);
}

fn(); // this指向window
new fn(); // this指向fn本身
const obj = { a: 123 };
const bindObj = fn.bind(obj);
new bindObj() // 覆蓋原先的this(obj),還是指向fn本身
new fn().bind(obj) // TypeError: (intermediate value).bind is not a function ... 無法在new時binding

// es6 class也是一樣的
class fn {
  constrcutor() {
     console.log(this)
  }
}

fn(); // TypeError: Class constructor test cannot be invoked without 'new' ...

new fn(); // this指向fn本身

使用bind, call, apply可以將this強制轉為輸入的參數

function fn() {
  console.log(this.a);
}

var a = 2;
fn(); // 2 <- 指向window, 在嚴格模式會回傳typeError

const obj = { a: 123 };
fn.call(obj); // 123
fn.apply(obj); // 123
const bindObj = fn.bind(obj);
bindObj(); // 123

呼叫時有context object的則以context object的環境為this

const obj = {
  a: 123,
  fn: function() {
    console.log(this.a);
  }
}

obj.fn(); // 123, 指向obj
const obj2 = obj.fn;
obj2(); // undefined, 指向window, 因為沒`context object`, 嚴格模式則會回傳typeError

obj.fn = function() {
  setTimeout(function() {
    console.log(this.a); // 這個function沒有context object, 會指向全域
  });
}

obj.fn(); // undefined, 指向window, 因為沒`context object`, 嚴格模式則會回傳typeError

沒有上述條件的,默認的this通常為global, 若為嚴格模式,則thisundefined

function fn() { 
  console.log(this.a);
}

fn(); // undefined, 指向window, 嚴格模式則會回傳typeError

關於屬性描述符 (Property Descriptors)

獲得Object的屬性Object.getOwnPropertyDescriptor

const obj = {
  a: 2
};

Object.getOwnPropertyDescriptor(obj, 'a');
/**
* {
*   value: 2
*   writable: true
*   enumerable: true
*   configurable: true
* }
**/

定義Object的屬性Object.defineProperty

const obj = {};
Object.defineProperty(obj, 'a', {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true
});

console.log(obj); // {a : 2}

可寫性 (Writable)

可以控制Object的property能不能被改寫

const obj = {};
Object.defineProperty(obj, 'a', {
  writable: false // 不能改寫
});

obj.a = 3; // 結果obj.a還是2,且在嚴格模式下會是TypeError: Cannot assign to read only property 'a' of object

可配置性 (Configurable)

決定Object是否可以調用defineProperty來調整定義,且configurable設為false, 就不能再設回true,屬於單向操作。

const obj = {};
Object.defineProperty(obj, 'a', {
  configurable: false // 不可配置
});

Object.defineProperty(obj, 'a', {
  configurable: true
}); // TypeError: Cannot redefine property

// 但是若是將writable從true改為false卻是可行的
Object.defineProperty(obj, 'a', {
  writable: false // true -> false 可行
});

// 不過將writable從false變true則會失敗
Object.defineProperty(obj, 'a', {
  writable: true // false -> true 不可行
}); // TypeError: Cannot redefine property

可枚聚性 (Enumerable)

決定Object在for..in時,會不會顯示,也就是可不可以在迭代中出現。

const obj = {
	a: 2,
  b: 3,
  c: 4,
  d: 5
};

Object.defineProperty(obj, 'a', {
  enumerable: false // 設定a不會被遞迴
});
for(let i in obj) {
	console.log(i);
}
// b
// c
// d

obj.propertyIsEnumerable('a'); // false

防止擴展 (PreventExtensions)

使用Object.preventExtensions可以防止Object被添加新的屬性

const obj = {
	a: 123
};

Object.preventExtensions(obj);

obj.b = 2; // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible

封印 (Seal)

使用Object.seal等同於同時使用Object.preventExtensions跟將configurable設為false

const obj = {
	a: 123
};

Object.seal(obj);

obj.b = 2 // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible

Object.defineProperty(obj, 'a', {
  configurable: true
}); // TypeError: Cannot redefine property

凍結 (Freeze)

使用Object.Freeze等同於同時使用Object.seal跟將writable設為false

const obj = {
	a: 123
};

Object.freeze(obj);

obj.b = 2 // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible

obj.a = 234; // 結果obj.a還是234,且在嚴格模式下會是TypeError: Cannot assign to read only property 'a' of object

存在性 (Existence)

使用Object.prototype.hasOwnPropertyin都可以檢查屬性是否在Object中,但in會額外查詢prototype

const obj = {
  a: 123
};

console.log('a' in obj); // true
console.log('toString' in obj); // true, 因為toString在prototype中

// 不使用obj.hasOwnProperty是為了避免obj的prototype有被竄改的問題
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true

console.log(Object.prototype.hasOwnProperty(obj, 'toString')); // false, 因為只檢查property

// 這時大家應該會想測試看看enumerable是不是會影響,答案是不會影響
obj.b = 345;
Object.defineProperty(obj, 'b', {
  enumerable: false
});
for (let i in obj) { console.log(i); } // a
console.log(Object.toString.propertyIsEnumerable()); // false, 所以for..in不會顯示
console.log('b' in obj); // true
console.log(Object.prototype.hasOwnProperty.call(obj, 'b')); // true

Object的其他應用

這個小節紀錄一些Object可以使用的方法

const obj = {
	a: 1,
  b: 2,
  c: 3
};

Object.keys(obj); // ['a', 'b', 'c']
Object.values(obj); // [1, 2, 3]
Object.entries(obj); // [['a', 1], ['b', 2], ['c', 3]]

for(let i in obj) {
	console.log(i);
}
// a
// b
// c

for(let i of obj) {
	console.log(i); // TypeError: obj is not iterable
}

/* 試著實作Symbol.iterator到Object */
obj[Symbol.iterator] = function() {
        const values = Object.values(this);
        let index = 0;
        return {next: function() {
            return { 
				value: values[index],
				done: index++ === values.length
			  };
        }
    }
}

const it = obj[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
it.next(); // { value: undefined, done: true }

for(let i of obj) {
	console.log(i);
}
// 1
// 2
// 3 <- 跑到done為true就不會再繼續顯示了

關於prototype

每個Object都內建prototype,並且像是chain一樣,一層一層的連結

難以捉模的prototype

一個Object在呼叫函式或變數時,若是找不到時,會往父層的prototype尋找,直到盡頭Object.prototype為止

// 定義了obj有a屬性
const obj = {
	a: 123
};

// 呼叫了obj沒有定義的toString
obj.toString(); // [object Object], 可以成功的原因是因為這個方法來自於Object.prototype.toString

釐清prototype之間的關係

  • A instanceof B代表B.prototype是否在Aprototype chain
  • __proto__代表該Object指向的prototype位置
function Fn() {};
// __proto__就是建構者的prototype
Fn.__proto__ === Function.prototype // true

// fn創建時會將__proto__指向Function.prototype
Fn instanceof Function; // true

// 而Function本身的__proto__是指向Object.prototype
Fn instanceof Object; // true

// fn本身沒constructor, 但指向的prototype有,所以來自Function.prototype,且Fn是由Function建構
Fn.constructor === Function; // true

// Function.prototype在Fn的prototype chain上嗎
Function.prototype.isPrototypeOf(Fn); // true

// 拿取創建Fn的prototype, 也就是Function的prototype做比較
Object.getPrototypeOf(Fn) === Function.prototype; // true
// Fn有自己的prototype, Function也有自己的prototype
// 且Fn的__proto__指向Function.prototype
// 所以Fn.prototype不會影響到Fn的取值,只有Function.prototype會
// ex: Fn.prototype.test = 1, Function.prototype.test = 2
// Fn.test會是回傳2
Fn.prototype === Function.prototype; // false

// 從以上推下來後,來測驗一下
const fn = new Fn();
fn instanceof Fn; // true
fn.constrctor = Fn; // true
fn.__proto__ === Fn.prototype; // true

prototype不為人知的設定

const obj = {};
obj.a = 1;

這個賦值的行為,會觸發一些事情,當a原本不存在在obj裡,但存在在obj指到的prototype中時,會觸發下列判斷

  • 訪問到prototype中的a,且awritabletrue,則會創建一個aobj中。
  • 訪問到prototype中的a,且awritablefalse,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤。
  • 訪問到prototype中的a,且a是個setter,則不會像第一個一樣,被重複創建,而只是被呼叫而已。

訪問到prototype中的a,且awritabletrue,則會創建一個aobj

const obj1 = { a: 1 }; // { a: 1 }
const obj2 = Object.create(obj1); // {} , prototype指向obj1
console.log(obj2.a); // 1
obj1.a = 2;
console.log(obj2.a); // 2
obj2.a = 3;
console.log(obj1.a); // 2
console.log(obj2.a); // 3

訪問到prototype中的a,且awritablefalse,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤

const obj1 = Object.defineProperty({}, 'a', {
    value: 1,
    writable: false
});
const obj2 = Object.create(obj1);

obj2.a = 2; // 不會有作用,且在嚴格模式時會拋出TypeError: Cannot assign to read only property 'a' of object

訪問到prototype中的a,且a是個setter,則不會像第一個一樣,被重複創建,而只是被呼叫而已

const obj1 = Object.defineProperty({b: 10}, 'a', {
    get: function() { return this.b * 2; },
    set: function(val) { this.b = val; }
}); // { b: 10};
const obj2 = Object.create(obj1); // {}

console.log(obj1.a); // 20
console.log(obj2.a); // 20
obj1.a = 2;
console.log(obj1.a); // 4
console.log(obj2.a); // 4
obj2.a = 4;
console.log(obj1.a); // 4
console.log(obj2); // { b: 4 }
console.log(obj2.a); // 8, 會不一樣是因為b在obj2.a的setter中創建在obj2中了

Css系列 - 如何避免邊界重疊 ( Margin Collapsing )

參考文章 MDN 理解邊界重疊的原因

邊界重疊 (Margin Collapsing)

當兩個block都有設定邊界,且兩個邊界重疊時,只留下最大值的邊界,這個情況就是邊界重疊
但設有float, position: absolute的元件不會發生上述的事情

有三個情況會造成邊界重疊

  1. 同一層相鄰時若設定邊界時,上下邊界會造成重疊
  2. 父元素與第一個或最後一個子元素若設定邊界時,上下邊界會造成重疊
  3. 兩個元素中間有空的元素時,若這兩個元素有設定邊界,上下邊界會造成重疊

同一層相鄰時若設定邊界時,上下邊界會造成重疊

// .scss
.mt-16 {
  margin-top: 16px;
}

.mb-16 {
  margin-bottom: 16px;
}
<!-- html -->
<section>
  <div class="block mb-16">margin-bottom: 16px</div>
  <div class="block mt-16">margin-top: 16px</div>
</section>

雖然兩個都設定margin,但因為相鄰,所以上下邊界會重疊,
就不是預想的32px而是16px了,以下圖例,綠色為margin的範圍(16px)

截圖 2020-06-03 下午7 49 27

父元素與第一個或最後一個子元素若設定邊界時,上下邊界會造成重疊

// .scss
.mt-16 {
  margin-top: 16px;
}
<!-- html -->
<section class="mt-16">
  <div class="block mt-16">margin-top: 16px</div>
</section>

當父元素有設定margin, 第一個元素也有設定margin,
父元素為block且沒有設定border, padding, overflow時,
就會變成第一個元素與父元素共享同個margin,所以不會分開來,就重疊了,
以下圖例,綠色為margin的範圍(16px)

截圖 2020-06-03 下午7 55 04

兩個元素中間有空的元素時,若這兩個元素有設定邊界,上下邊界會造成重疊

// .scss
.mt-16 {
  margin-top: 16px;
}

.mb-16 {
  margin-bottom: 16px;
}
<!-- html -->
<section>
  <div class="block mb-16">margin-bottom: 16px</div>
  <div class="block mt-16">margin-top: 16px</div>
</section>

雖然兩個元素沒有相鄰,但因為空的元素導致他們實質上是相鄰的,所以會有case1的事情發生,
以下圖例,綠色為margin的範圍(16px)

截圖 2020-06-03 下午7 55 57

原檔程式碼

Angular小教室 - 用Note List了解頁面基本元素 (基礎篇)

這次要介紹的是,用Angular打造的App到底是怎麼組成的,以下是官網Angular Architecture Overview示意圖

從圖中我們可以看出來,Angular App主要由這八個元素構成

  1. Metadata(描述資料)
  2. Dependency Injection(倚賴注入,簡稱DI)
  3. Service(服務)
  4. Template(模板)
  5. Directive(指令)
  6. Component(元件)
  7. Data Binding(資料綁定)
  8. Module(模組)

以下我們就會講解這八個元素提供什麼樣的功能,扮演什麼角色以及如何使用,那在說明前還需要先暸解什麼是Decorator(裝飾器),因為這跟等一下要說明的元素也有一些關聯性。

Decorator(裝飾器)

  • 設計模式: Decorator Pattern(裝飾者模式)
    • 說明: 主要是在原有的事物上做擴充,那因為在實作上是包裹原先的事物來擴充,所以看起來就像是在身上裝飾了些什麼,故將此設計叫做裝飾者模式。
  • 用途:藉由註記(annotations)描述資料(metadata)將原本的類別加工,讓類別在閱讀與擴充上更優雅。
    • 註1: Typescript Document Decorator的解釋為Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.
    • 註2: meta-programming syntax 代表產生程式的程式碼語法,猜測是使用@語法糖創造程式碼的意思
  • 使用方式: @<function>
  • 範例:
/*
 * 此為簡易擴充類別的decorator function
 * 更深入的講解會在後續章節提到
 * 重點在下方的註解想表達的意思
 */
function Job(job) {
    return (person) => {
        return (...arg) => {
          const personWithJob = new person(...arg);
          personWithJob.title = job.title;
          personWithJob.salary = job.salary;
          return personWithJob;
        }
    }
}

@Job({
  title: 'Engineer',
  salary: 50000
})
class Person {}
const john = new Person();
/* @Job就是註記(annotations)
 * 讓我們知道John這個人(Person)有工作(Job)
 * 然後藉由描述資料(metadata) => {title: 'Engineer', salary: 50000}
 * 了解這個工作名稱是工程師,薪水大概是50000
 */ 
  • 緣起: es6增加了類別與繼承的概念,但在共享一些功能時,顯得不是那麼的優雅與易懂,於是在es7引入Decorator的語法,藉由註記(annotations)描述資料(metadata)的方式讓程式碼更加優美。
  • 總結: Typescript支援Decorator,Angular本身是基於Typescript打造,於是Angular將Component、Module、Service、Directive等元素都是使用Decorator來進行擴充,那Decorator的語法糖是使用@,所以在Angular程式碼中看到@Component@NgModule這些都是Angular為了做一些整合與共用而定義好的裝飾器。

Metadata (描述資料)

metadata其實有很多中譯,如元數據、元資料、後設資料、描述資料等等,那這邊我是覺得描述資料很對我胃口,所以使用他來當做中文翻譯的詞

metadata就像是上面裝飾器的titlesalary一樣,用於描述這個Job可以再附加的資料,那Angular的裝飾器也都是如此,所以當看到範例中...的部分,就是大家口中說的metadata

  • Angular常見的裝飾器
    @Component({
        ...
    })
    
    @NgModule({
        ...
    })
    
    @Directive({
        ...
    })
    
    @Injectable({
        ...
    })
    
    @Pipe({
        ...
    })
    

Dependency Injection (倚賴注入)

  • 說明: DI是個Design Pattern(設計模式),主要是為了將功能集中,相依性減少的設計,也就是低耦合高內聚的設計。
    低耦合代表跟外部的程式碼關係小,如登入服務註冊服務可能流程上有關,但內部程式碼卻不會互相影響,如這時我的註冊服務壞了,但因為不影響登入服務,所以使用者只是不能註冊但還是可以登入。
    高內聚代表跟內部的程式碼關係緊密,就如上面登入服務註冊服務,只專注負責一個功能登入服務就是只有登入時會需要他,他就是獨立的個體,同理可知註冊服務也是如此。
  • 用法: 藉由Class(類別)的constructor(建構子)來inject(注入)Service(服務)
  • **: 希望來源是從外部引入,而不是由自己創造。
  • 範例: 靈感來自大神的範例
    /*
     * 無DI幫助的情況下,有以下三個缺點
     * 1. 如AuthService的constructor參數增加了,所有用到AuthService的都需要更動
     * 2. LoginService因為內部寫死AuthService只能提供google認證的登入
     * 3. 單元測試無法隔離AuthService
     */
     class LoginService {
         constructor() {
             this.auth = new AuthService('google');
         }
         login() {
             // call login api
             ...
             // check auth
             this.auth.checkAuth(token);
         }
     }
     
     const loginService = new LoginService();
     loginService.login();
    /* 
     * 在constructor傳入物件(authService)的方式即為DI
     * 改善未使用DI的缺點
     * 1. 因為是在new LoginService才決定輸入,所以只要在邏輯處統一做調整就好
     * 2. 因為是在new LoginService才決定輸入,所以可以隨意改變input
     * 3. 因為是在new LoginService才決定輸入,所以測試時可以傳一個MockAuthService來做到隔離外部程式碼
     */
    class LoginService {
         constructor(authService) {
             this.auth = authService;
         }
         login() {
             // call login api
             ...
             // check auth
             this.auth.checkAuth(token);
         }
     }
     const loginService = new LoginService(new AuthService('facebook', {logger: true}));
     loginService.login();
    
    補充: Angular 官網文件 Dependency Injection in Angular

Service(服務)

  • 說明: 在Angular中扮演著處理邏輯的角色,並且良好的Service(服務)應該只針對特定事情做處理,如介紹DI時講到的登入服務註冊服務就是如此。
  • 範例:
/*
 * 如何定義一個Service
 * 在Angular中Service可以被其他類別注入
 * 那要讓Angular知道這個Service可以被DI,就要使用裝飾器來達成目的
 * @Injectable()這個裝飾器,告訴Angular這個服務是可被注入的
 * 那Angular有自己的DI框架,是藉由注入Service的type(類型)來判斷要引入哪個服務
 * 如下就是辨別LoginService這個類型來讓Angular知道要從哪裡引入服務
 */
 @Injectable({
     providedIn: 'root'
 })
 class LoginService {}
 
 @Component({
     ...
 })
 class User {
     constructor(private loginService: LoginService) {}
 }
  • 補充1: @Injectable只有一個metadata就是providedIn
    • root
      • 是代表在App啟動時就被創造等待其他元件注入了,若是沒有被注入,那會在編譯時因為tree-shaking將服務移除,到上一章複習tree-shaking用途
    • specificModule
      • 假設我指定LoginModule,那麼只有當我import LoginModule時才可以使用這個服務,值得注意的是LoginModule中也無法使用,因為沒有import自己
      • 範例:
        // 代表只有import LoginModule才能使用
        @Injectable({
            provide: LoginModule
        })
        class LoginService {}
        
  • 補充2: 除了在@Injectable()使用providedIn這個metadata以外,還可以獨立在ComponentModule使用
    • 如果是在內部Module或是Component使用時,都會重新創造一個新的Service,也就是說如A跟B同時都對自己提供了一個Service,那兩個就是獨立的個體,A的Service改變,也不會影響到B的Service
    // Module provider使用方式
    @NgModule({
        ...
        providers: [LoginService]
    })
    
    // Component provider使用方式
    @Component({
        ...
        providers: [LoginService]
    })
    

Template(模板)

Template主要用途就是呈現使用者看到的介面,並賦予程式可以與其互動溝通,以下為幾個重要名詞

  • interpolation(插值)
    • 主要是用來呈現變數,並以{{變數放這裡}}的符號來辨識,以下幾個範例可以看看
      /*
       * Component variable(元件變數)
       * Component就是跟Template互動的邏輯部分,一樣往下滑有介紹
       * 將Component中的參數或是函式,透過Template輕鬆地呈現
       * {{}}會辨識內容中的字串比對Component中的變數或函式
       * 
       * 網頁上看到的樣子
       * 名字: 小明
       * 打個招呼: 哈囉
       * /
      
       .html
       <div>名字: {{name}}</div>
       <div>打個招呼: {{greeing()}}</div>
      
       .ts 
       @Component({
           ...
       })
       export class HelloCopmonent {
          name = '小明';
          
          greeting() {
              return '哈囉';
          }
       }
       
      
      
      /*
       * Template input variable(模板輸入變數)
       * *ngFor是結構指令,好奇的可以先往下滑,有介紹
       * 這邊是元件變數將list的值都拿出來
       * 然後藉由{{}}來將模板輸入變數item呈現在頁面上
       */
      <div *ngFor="let item of list">{{item}}</div>
      
      /*
       * Template reference variable(模板參考變數)
       * 想要直接在模板中拿取DOM的值也是非常容易
       * 只要藉由#這個符號,配上你想命名的變數名稱
       * 就可以拿到模板上的參考,並藉由這個參考來做運用
       * 以下是拿取input的內容顯示到頁面的範例
       * 
       * 頁面上大概會呈現這樣,有框框的是輸入框的示意圖
       *  ----------
       * | 我是input |
       *  ----------
       * 我是input
       */
       <input value="我是input" #input>
       <div>{{input.value}}</div>
      
  • 優先權順序:
    • template input variable = template reference variable > component variable
      /*
      * 輸出結果為
      * 我覆蓋了component variable
      * 我覆蓋了component variable
      * 1
      * 2
      * 
      * 原先的component variable被template reference variable覆蓋
      * 所以顯示的是"我覆蓋了component variable"
      */
      .ts
      @Component({
          ...
      })
      class PriorityComponent {
         variable = {textContent : 'component variable'};
      }
          
      .html
      <div>{{variable.textContent}}</div>
      <div #variable>我覆蓋了component variable</div>
      <div *ngFor="let variable of [1,2]">{{variable}}</div>
      
  • 可以更改interpolation的辨識符號
    • 更改Component metadata中的interpolation
      /*
       * 將@@##覆蓋原先的{{}}
       * 
       * 所以頁面會顯示
       * 50 + 50 = 100
       */
      .ts
      @Component({
          interpolation: ['@@', '##']
      })
      class TestComponent {}
      
      .html
      <div> 50 + 50 = @@50+50## </div>
      
  • template expression(模板表達式)
    • 以下為簡單列出支援與不支援的邏輯運算,更多資訊可以參考Angular Template Expression
      {{1 + 1}} // 2
      {{null || 123}} // 123
      {{true ? 'yellow' : 'red'}} // yellow
      
      {{String(123)}} // 不支援
      {{a++}} // 不支援
      {{new hello()}} // 不支援
      
    • 雖然表達式支援一些邏輯運算,但盡量保持以下三個原則
      • No visible side effects(無看得見的副作用)
        • 就是在頁面上使用模板呈現結果時,若是在呈現A時不要影響到B原來呈現的值
        /*
         * 因為sideEffect在呈現時會執行一次(將 a + 1)
         * 所以會影響到原本已經render(渲染)好的a值
         * 雖然畫面會做更動,但不一定會是預期的數值
         * 且瀏覽器上會出現紅字警告
         * 因此強烈建議不要影響到其他的變數
         */
        .ts
        @Component({
            ...
        })
        class SideEffectComponent {
            a = 1;
            b = 2;
            sideEffect() {
                this.a = this.a + 1;
                return this.b;
            }
        }
        .html
        <div>{{a}}</div>
        <div>{{sideEffect()}}</div>
        
      • Quick execution (快速執行)
        • template expression上的數值的改變是藉由Angular的偵測,如使用者點擊、拖曳、呼叫API、setTimeout,等非同步的行為都會被偵測到並馬上改變interpolation,所以若是代價比較高的運算,建議是使用一些變數來暫存這些值。
      • Simplicity (看起來簡單)
        • 雖然expression可以放複雜的邏輯,但Template主要是用來展現UI的,且過多的邏輯也會造成測試上的不易
  • Angular提供的template

    ng-templateng-container多用於Structural Directive(結構指令)建議可以參考下一節的Directive範例服用

    • ng-template
      • 使用場景: 本身是存在於Template上但不顯示於DOM中,主要用於Structural Directive (結構指令)語法糖*的轉換過程中的載體,也會被使用在動態載入Component時的載體,若無搭配結構指令或是動態載入,本身是不會顯現在畫面上
        /*
         * ng-template本身沒搭配結構指令,是不會顯示在畫面上的
         * 所以畫面中不會顯示123
         * 網頁畫面
         * 456
         */
        <ng-template>123</ng-template>   
        <ng-template [ngIf]="true">456</ng-template>
        
    • ng-container
      • 使用場景: 當沒有合適的元素可以當結構指令的載體時,就需要使用這個模板,ng-containerng-template最大的不同是,不需要搭配結構指令也可以顯現在UI上
        /*
         * 網頁畫面
         * 123
         * 456
         */
         <ng-container>
             <div>123</div>
         </ng-container>
         <ng-container *ngFor="let item of [4,5,6]">{{item}}</ng-container>
        
    • ng-content
      • 使用場景: 用於顯示Component中的元素
        /*
         * selector是component在template上的名稱
         * template是component要呈現在UI上的樣子
         * yur-parent的tag包覆的內容,即為ng-content會呈現的內容
         * 
         * 網頁顯示
         * 下面是ng-content的內容
         * 我是ng-content
         */
        @Component({
          selector: 'yur-parent',
          template: `
          下面是ng-content的內容
          <ng-content></ng-content>
          `
        })
        class ParentComponent {}
        
        main.html
        <yur-parent>我是ng-content</yur-parent>
        

Directive(指令)

文中會說到DOM,所以簡易說明一下
Document Object Model(文件物件模型,簡稱DOM),提供HTML,XML,SVG結構化介面,提供程序存取與改變文檔結構與內容的方法。

Directive有三種呈現的形式

  • Structural Directive(結構指令)
    • 說明: 會對DOM的結構中進行新增刪除DOM元素的指令,皆稱為結構指令
    • Angular為了避免結構指令產生複雜的程式碼,所以加入了*(星號)的語法糖,以下將介紹*與三個常見的結構指令。
      • *
        • 說明: 為了使程式碼更簡潔成出現的語法糖,在結構指令的前綴加上*時,Angular會在HTML渲染時,將結構指令轉移到ng-template(Angular提供的元素)上,並改使用property binding(屬性綁定)來呈現。
        • 流程:
          • 將帶有星號的結構指令從原先的元素移出來,接著藉由屬性綁定的方式綁定在ng-template
          • 移除結構指令的元素移進ng-template
        • 範例: 可參考後續的三個結構指令。
      • NgIf
        • 說明: 藉由boolean值決定是否新增或刪除Dom元素,因為是刪除Dom元素,也就是不會在Dom找到,這也就是跟css style裡distplay:none只是隱藏起來最大的差別,至於為什麼是刪除而不是隱藏,可以參考Angular - Why remove rather than hide
        • 範例
        // *(星號)語法糖的呈現方式,大多使用此方法較簡便
        <div *ngIf="isActive">
            isActive是true我就會顯示,反之則不會出現在DOM中
        </div>
        
        /* 
         * Angular根據*ngIf轉換為以下程式碼
         */
        <ng-template [ngIf]="isActive">
            <div>isActive是true我就會顯示,反之則不會出現在DOM中</div>
        </ng-template>
        
      • ngForOf
        • 說明: 主要用於顯示清單上的資料,如資料結構相同但品名不同就適合使用此指令,若使用*語法糖看到的會是*ngFor,那依放置的元素決定要重複的區域。
        • 範例:
        /* 
         * *(星號)語法糖的呈現方式
         * 在此使用ngFor提供的模板輸入變數,animal與i
         * 顯示陣列中的animal(內容)與index(索引值)
         * 程式語意大致為,幫我將陣列內容印下來並幫我打上index(索引)
         * 顯示結果如下
         * 1. dog
         * 2. cat
         * 3. cow
         * /
        
        <div *ngFor="let animal of ['dog', 'cat', 'cow']; let i=index;">
            {{i+1}}. {{animal}}
        </div>
        
        /*
         * Angular根據*ngFor轉換為以下程式碼
         */
        <ng-template let-animal  let-i="index" [ngForOf]="animals">
            <div>{{i+1}}.{{animal}}</div>
        </ng-template>
        
        • 補充: ngForOf不只提供index(索引)的參數,其他好用的參數可至Angular ngForOf API查詢
    • ngSwitch
      • 說明: 基於javascript中switch的原理,根據輸入決定要顯示的元素
      • 範例
        /*
         * *(星號)語法糖的呈現方式
         * 輸入為yellow,輸出則根據輸入會顯示yellow
         * 若輸入都不相符時,則會顯示pink當作預設輸出
         * 那red旁邊的'符號是因為綁定的是Component的屬性
         * 所以為了轉換為字串所以添加'...'讓angular知道是字串
         */
        <div ngSwitch="yellow">
            <div *ngSwitchCase="'yellow'">yellow</div>
            <div *ngSwitchCase="'red'">red</div>
            <div *ngSwitchCase="'blue'">blue</div>
            <div *ngSwitchDefault>pink</div>
        </div>
        
        /*
         * Angular根據*ngFor轉換為以下程式碼
         */
         <div ngSwitch="yellow">
              <ng-template ngSwitchCase="yellow">
                  <div>yellow</div>
              </ng-template>
              <ng-template ngSwitchCase="red">
                  <div>red</div>
              </ng-template>
              <ng-template ngSwitchCase="blue">
                  <div>blue</div>
              </ng-template >
              <ng-template ngSwitchDefault>
                  <div>pink</div>
              </ng-template>
         </div>
        
      • 補充ngSwitch是屬性指令,只是作為輸入來影響下面的結構指令並無直接對DOM進行增加移除,也就是不能使用*ngSwitch來使用
  • Comonent(元件)
    • 說明: Directive(指令)與Template(模板)結合的產物,所以說元件是指令的一種延伸。
    • 兩者差別性
      • 指令是對原先已經存在的元素下去對DOM做添加、修改與刪除
      • 元件是直接在DOM中創造新的UI與行為
  • Attribute Directive(屬性指令)
    • 說明: 對元素或元件本身做外觀或行為上的改變,或是其他的指令,皆稱為屬性指令
    • 範例:
    /* 
     * 創造一個綁定屬性的yurHighLight的指令
     * 因為是綁定在某個元素之上
     * 這時可以直接取用該元素上的DOM進行操作
     * 這邊是將顏色highlight為紅色
     * 在constructor創造參數的方式稱為DI(倚賴注入)
     */
    @Directive({
      selector: '[yurHighlight]'
    })
    export class YurHighlight {
      constructor(private el: ElementRef) {
        el.nativeElement.style.color = 'red';
      }
    }
    

Component(元件)

Component在Angular中是最小的UI單位,一個頁面是一個至多個Component組成,Component是Directive + Template的組合,繼承於Directive的屬性並擁有Template可以呈現UI,以下介紹幾個好用的metadata

  • selector
    • 使用Component時不能省略selector,為必要參數
    • 用於辨別Component的名稱,如Component的selector是yur-hello,則template上出現<yur-hello></yur-hello>時,Angular會自動將屬於yur-hello這個Component的template取代上去
  • template
    • 使用Component不能省略template或是templateUrl其中一個metadata,否則無法使用
    • 屬於inline的template,可以直接在同一個檔案裡使用template
      @Component({
          ...
          template: `
          <div>我是inline template</div>
          `
      })
      
  • templateUrl
    • 使用Component不能省略template或是templateUrl其中一個metadata,否則無法使用
    • 使用外部檔案的template
      // 使用login.component.html的外部檔案當template
      @Component({
          ...
          templateUrl: './login.component.html'
      })
      class LoginComponent
      
  • styles
    • 在Component中使用inline style,也就是可以在同一個檔案直接寫style
    • 輸入為陣列,所以可以輸入多個style
    @Component({
        ...
        styles: [
         `
         div {
             color: red;
         }
         `
         span {
             color: blue;
         }
         ,
         `
         div {
             font-size: 12px;
         }
         `
        ]
    })
    
  • styleUrls
    • 使用外部檔案的style
    • 輸入為陣列,所以可以輸入多個style
      // 使用login.component.scss的外部檔案當style
      @Component({
          ...
          styleUrls: ['./login.component.scss']
      })
      class LoginComponent
      
  • encapsulation
    • 用來決定Component style的封裝特性,以下為其輸入參數
      • ViewEncapsulation.ShadowDom
        • Shadow DOM有幾個重要特點
          • 與DOM隔離
            • 無法藉由document方法(ex:querySelector, getElementById, etc.)取得shadow dom的節點
          • 作用域的Style
            • 在Component設定的style不會影響外部的其他Component,也不會滲入在Component中被呼叫的子Component
          • 簡化命名
            • 鑑於作用域的style,所以在style的命名上可以更簡化
        • 原先叫做ViewEncapsulation.Native,於angular 6棄用
        • 利用shadow dom來達到每個Component中style互不干涉
      • ViewEncapsulation.Emulated
        • 預設值
        • 模仿Shadow Dom的特色,達到style互不干涉
        • 在build時,將style加入特殊的attribute(ex: _ng-content-c1),藉此來區別每個Component中style是不相同,也就完成了封裝的特性
        • ex:
          .css
          .hello {
              color: white;
          }
          // 處理後的結果
          .hello[_ng-content-c1] {
              color: white;
          }
          .html
          <div class="hello">hello</div>
          // 處理後的結果
          <div class="hello" _ng-content-c1 >hello</div>
          
      • ViewEncapsulation.None
        • 完全不將style封裝,也就是說style的設定會影響到整個頁面而不是只在Component本身
  • providers
    • 輸入為陣列,所以可以輸入很多Service
    • 依賴Component的Service,當Component被創造出來時,連帶也會創造屬於Component專用的Service。
  • viewProviders
    • 輸入為陣列,所以可以輸入很多Service
    • 依賴Component的Service,當Component被創造出來時,連帶也會創造屬於Component專用的Service。
    • providers差異性
      • 若是在LoginComponent使用ng-content來呈現子Component,則子Component是不會吃到提供給LoginComponent的Service,簡而言之就是除了Component本身,有出現在template上的Component也會得到效益,反之若是使用ng-content的話則不會共用Service。
  • moduleId
    • 用於抓取Component的相對路徑
    • Angular預設是使用webpack(打包工具),所以不用設定moduleId,但若是不使用webpack時,可能需要使用moduleId:module.id來讓templateUrlstyleUrls來抓相對位置的路徑。

Pipe (通道)

Pipe就跟哆啦A夢的縮小隧道功能很像,經過縮小隧道人就會變小,那Pipe就是將輸入的值通過隧道轉換成另一個值,主要在Template上使用,使用方式

  • {{value | pipeName:input}}
    • valueinterpolation中綁定的數值
    • |為使用Pipe的前置符號
    • pipeName是pipe定義完後的名稱,在metadata的name設定
    • :input,若要針對pipe做一些參數控制,可以使用:加上你要的參數,若有兩個參數要輸入就是:input1:input2,也就是想輸入幾個就寫幾遍
    • 那Pipe如何轉換就需要implements PipeTransform,實作這個interface(介面),以下會介紹metadataPipeTransform用法
    • name
      • 說明: 定義Pipe在template上使用的名稱,不可省略此參數。
      • 範例:
        @Pipe({ name: sayHello })
        export class SayHelloPipe implements PipeTransform {
            transform(value, input) {
                ...
            }
        } 
        
    • pure
      • 說明: 輸入為boolean預設為true,當設定為true時,當數值的reference(參考)改變時,Pipe會偵測並重新對其做轉換,若設定為false時,只要數值改變就會對其做轉換,但可能會因為頻繁偵測的緣故導致效能變差,舉例一下差別
        • 範例:
          /*
           * 頁面顯示
           * 
           * Hello hubert
           *  -------
           * | click |
           *  -------
           */
          .ts
          @Component({...})
          export class SampleComponent {
              obj = {
                  name: 'hubert',
                  change: false
              }
          }
          .html
          <div>{{obj | sayHello}}</div>
          <button (click)="obj.change = !obj.change">click</button>
          
          @Pipe({
            name: 'sayHello',
            pure: ...
          })
          export class SayHelloPipe implements PipeTransform {
          
            transform(value: any, args?: any): any {
              return value.change ? `Hi ${value.name}`: `Hello ${value.name}`;
            }
          }
          
          • pure: true
            • 當我們點擊click,因為obj參考沒變更,只是更動change這個屬性值,所以Pipe不會偵測到變動,所以不會有轉換的行為,所以畫面不會變
          • pure: false
            • 雖然obj參考沒變更,只是更動change這個屬性值,但因為pure設定為false所以Pipe會頻繁偵測,所以會對更動的值做轉換的行為,所以畫面的hello hubert會變更為hi hubert
    • PipeTransform
      • 說明: 使用Pipe必須實作這個介面
      • 範例:
        /*
         * transform為實作PipeTransform這個interface要實作的function
         * 第一個參數value為想轉換的數值,ex: {{name | sayHello}}
         * name就是value的輸入參數
         * 後續的input1, input2等就是看是否要給pipe輸入
         * ex: {{name | sayHello: 'ok'}}則input1為'ok'
         * ex2: {{name | sayHello: 'ok': 'no'}}則input1為'ok', input2為'no'
         * ex3: {{name | sayHello}}則不會有任何input
         * return的值,即為轉換的數值
         */
        @Pipe({ name: sayHello })
        export class SayHelloPipe implements PipeTransform {
            transform(value, input1, input2, ....) {
                return value.change ? `Hi ${value.name}`: `Hello ${value.name}`;
            }
        } 
        
    • 補充:

Module (模組)

Module就像是一個打包好的功能,這個打包好的功能可以給其他的module使用,自身也可以導入其他的Module,本身Module包含一些Component(元件),依賴在Module本身的Service,也就是說Module可以分享在module中的Component和共享Service給其他Module,那在Angular中若想要製作成Module,可以使用已經定義好的Decorator(裝飾器)NgModule,以下會介紹NgModule常用的metadata

  • imports
    • 說明: 用來接收Module,其目的是Moduleexport出來的物件,以達到功能模組化,藉由import慢慢把小功能組成大功能,這時大家可能會想說,import到重複的會發生什麼事?!
      1. 假設OneModule,TwoModule都import ThreeModule
      2. FourModule import OneModule和TwoModule
      3. 這時會發現ThreeModule重複import好幾次
      4. 但Angular有幫我們做好機制,同一個Module只會被import一次,所以就不用擔心,好好的分門別類大力的使用import吧XDD
    • 範例:
      /*
       * 範例為import LoginModule和SharedModule
       * 所以若LoginModule有export LoginComponent
       * 就可以在頁面上使用LoginComponent和YurHighlight了
       * 輸入為陣列,所以可以export多個
       */
      @NgModule({
          imports: [LoginModule, SharedModule]
      })
      export class ExampleModule {}
      
  • exports
    • 說明: 當OneModule導入(import)TwoModule時,OneModule能得到的物件就是TwoModule從exports定義來的,如ComponentDirectivePipeModule,那exportModule的作用是什麼?
      • 當OneModule定義export有TwoModule時,則ThreeModule在import OneModule的同時,也import到TwoModule的功能了
    • 範例:
      /*
       * 範例為export Component和Directive
       * 所以若有Module import ExampleModule
       * 就可以在頁面上使用LoginComponent和YurHighlight了
       * 輸入為陣列,所以可以export多個
       */
      @NgModule({
          exports: [LoginComponent, YurHighlight]
      })
      export class ExampleModule {}
      
  • declarations
    • 說明: ComponentDirectivePipe,都需要再Module裡進行宣告,才能在template中被辨識到。
    • 範例:
      /*
       * 範例為定義Component和Directive
       * 輸入為陣列,所以可以定義多個
       */
      @NgModule({
          declarations: [LoginComponent, YurHighlight]
      })
      export class ExampleModule {}
      
    • 補充:
      • 一個component只能在一個module被定義,也就是當在別的module也要使用到這個component時,這時會有兩個方案
        1. 若是這個在功能性上不大,比較偏向是UI呈現不同或是很通用的component就會比較傾向於,將共用到的component拉出來在一個SharedModule裡export出來給其他兩個module使用
        2. 若是功能明確就是要在OneModule中的component,但TwoModule要卻要用到時,這時的情況可能會是OneModule是一個小頁面,但TwoModule是個大頁面,所以OneModule可以export要共用的component讓TwoModule可以import,否則的話應該會選擇第一個方案。
  • providers
    • 說明: 提供Service可以依賴在Module上,在介紹Service有提到在註冊到provider時會create一個實體,若OneModule跟TwoModule各自provider同一個Service,則參數互相不干擾,但provider本身提供的對象為Module內部的成員及export到的Module會共享同一個Service,也就是若OneModule跟TwoModule同時被ThreeModule import的話,則會共享同一個Service。
  • entryComponents
    • 說明: entryComponents的意義在於讓Angular知道這個元件是被需要的,為什麼知道被需要這件事是重要的,讓我娓娓道來
      • 首先Angular提供tree-shaking,所以沒用到的Component就會被移除,不管他是不是在Module定義了
      • 如何知道Component被用到,Angular在Component提供的template中找的到Component定義的selector就代表被用到
      • 既然在Template中被找到的就算有定義,那幹麻還要定義一個entryComponents讓Angular知道?
        • 因為Angular提供動態載入Component的方式,也就是頁面可能放著一個標示用的標籤,等載入時才判定要使用哪個Copmonent
      • 哪些會需要用到entryComponents
        • 初始化出來的AppComponent就要,雖然他在index.html有使用到selector,但那不是Component的template所以需要動態載入
        • 路由切換時,使用router-outlet的標籤當作容器,根據現在的路由位置決定要顯示哪個元件,這些會被顯示的Component Angular會自動將他們轉換為entryComponent,路由的使用方式後幾章會講到,這裡主要是了解使用到的路由是使用到entryComponent的方式
        • 除了以上兩種,當我們自己要動態載入Component時,也需要定義到entryComponent才能使用,後續有機會會講解用法
    • 不管定義在哪個Module,只要設定了entryComponents就會在載入Angular的同時就被加載
  • bootstrap
    • 說明: 雖然我們介紹了Module可以有很多個,但在APP開啟時,需要一個root module來當作頁面顯示的起點,bootstrap就是用來設定起始要顯示的Component,也只有在root module中才會設定到這個metadata,放在bootstrapComponent也會預設為entryComponents
    • 範例:
      /*
       * 範例為用CLI創建專案後的AppModule
       * AppModule為root module,所以呈現上
       * 會以AppComponent來當起點,再藉由慢慢import Module
       * template組合其他Component慢慢搭建起其他頁面
       * 輸入為陣列,所以可以定義多個,但目前看起來都只定義一個
       */
      @NgModule({
        declarations: [AppComponent],
        bootstrap: [AppComponent]
      })
      export class AppModule { }
      

Data Binding (資料綁定)

在template中可以與Component的變數和函式通過interpolation方式來進行互動,除此之外Angular也提供了一些事件和屬性上的綁定,以下會介紹三種綁定方式及如何自訂屬性和事件

  • proerty binding (屬性單向綁定)
    • 用途: 針對綁定的屬性給予輸入
    • 說明: 屬性綁定可針對Html標籤或是Component來做綁定,綁定的內容跟interpolation是一樣,可以是字串或是跟Component拿取參數跟函數,但若要使用Component的資料時,需要藉由使用[]這個符號將要綁定的屬性框起來,若沒使用[]則只會當作輸入是字串型態
    • 範例:
      .ts
      class SampleComponent {
          img = '123.png';
          class1 = 'class-one';
          class2 = 'class-two';
          attr1 = 'attr-one';
          style1 = {color: 'red', background: 'pink'}
      }
      
      <!-- img binding方式-->
      
      <img src="img">會轉換為&lt;img src="img"&gt;
      <img [src]="img">會轉換為&lt;img src="123.png"&gt;
      
      
      <!-- class binding方式-->
      
      <div class="class1">�會轉換為&lt;div class="class1"&gt;&lt;/div&gt;</div>
      <div [class]="class1">�會轉換為&lt;div class="class-one"&gt;&lt;/div&gt;</div>
      <div [class.class1]="true">會轉換為&lt;div class="class1"&gt;&lt;/div&gt;</div>
      
      
      <!-- style binding方式-->
      
      <div [style.width]="'200px'">會轉換為&lt;div style="width:200px"&gt;&lt;/div&gt;</div>
      <div [style.width.px]="200">會轉換為&lt;div style="width:200px"&gt;&lt;/div&gt;</div>
      <div [style.width.%]="100">會轉換為&lt;div style="width:100%"&gt;&lt;/div&gt;</div>
      
      
      <!-- attribute binding方式 -->
      
      <div [attr.data-title]="attr1">會轉換為&lt;div data-title="attr-one"&gt;&lt;/div&gt;</div>
      <div [attr.data-title]="'attr1'">會轉換為&lt;div data-title="attr1"&gt;&lt;/div&gt;</div>
      
      
      <!-- ngClass binding方式-->
      
      <div [ngClass]="[class1, class2]">會轉換為&lt;div class="class-one class-two"&gt;&lt;/div&gt;</div>
      <div [ngClass]="{ class2: true }">會轉換為&lt;div class="class2"&gt;&lt;/div&gt;</div>
      <div [ngClass]="class1">會轉換為&lt;div class="class-one"&gt;&lt;/div&gt;</div>
      
      
      <!-- ngStyle binding方式-->
      
      <div [ngStyle]="style1">會轉換為&lt;div style="color: red; background: pink;"&gt;&lt;/div&gt;</div>
      
  • event binding (事件單向綁定)
    • 用途: 利用函式對於事件的輸出做反應,主要用途是針對使用者行為做回饋
    • 說明: 事件綁定可對現有的HTML元素或是Component自定義的事件做綁定,並利用()符號中放入要監聽的事件,那接下來的說明比較繁瑣,所以用條列式的會比較好看
      • HTML事件都是on做開頭,但Angular在事件綁定時將on省略,如onclick在綁定時就會是(click),並且建議Component定義事件時不要使用on當開頭
      • 事件回傳的參數使用$event來接收,如(mouseover)就會收到型態為MouseEvent的Event Type
    • 範例:
      /*
       * 綁定onclick事件,觸發時呼叫Component中的onClick函式
       * 將MouseEvent的資料藉由$event傳給onClick這個函式
       * 函式名稱沒固定,也可以叫handlerClick($event)沒關係
       * 若是沒有要MouseEvent的資料,也可以把$event拿掉
       */
      <div (click)="onClick($event)">Click me!</div>
      
  • two way binding (屬性與事件雙向綁定)
    • 用途: 利用兩邊互相傳遞的方式,在事件觸發時連帶改變屬性,適合做一些連動的事情,如輸入框、下拉選單等。
    • 說明: 雙向綁定的符號為[()],雙向綁定就像是對屬性給予初始的輸入,然後觸發了綁定的事件,所以事件回饋給輸入一個數值,因此輸入的變數就變成了事件回饋的數值以使用NgModel的範例來看就是,一開始給文字欄空白的文字,但因為打字會觸發oninput事件,所以事件回傳了剛打的字,所以變數text就變為事件回傳的字。
      範例:
    /*
     * text會隨著輸入而改變數值
     * 此方式為同時使用屬性綁定與事件綁定,但不是雙向綁定的寫法
     */
     
    <input type="text" [value]="text" (input)="text=$event.target.value">
    <div>text: {{text}}</div>
    
    /*
     * text會隨著輸入而改變數值
     * 使用NgModel更簡潔地完成(two way binding)
     */
     
    <input type="text" [(ngModel)]="text">
    <div>text: {{text}}</div>
    
    • 補充:
      • ngModel是Angular提供的Directive
        • 使用時需在module(模組)import FormsModule才能使用
        • 支援大部分的input類型,如checkbox,radiobox,Angular上有介紹NgModel支援的類型
  • 如何自訂屬性及事件
    在自訂屬性及事件裡,需要知道兩個裝飾器,@Input@Output藉由這兩個裝飾器就可以做到自訂屬性及事件了。
    • @Input
      • 說明: 可以在Template中將屬性綁定到DOM上,也就是賦予Component擁有自定的屬性
      • 範例:
        /*
         * 以下範例的不同點,來自於@Input的metadata
         * 當沒給metadata時,component上的屬性input1會照原本命名
         * 當有指定名稱時,如指定input2則代表對外給予的綁定屬性為input2
         * 但是在component內部時,參數名稱則是input3
         * ngOnInit是Angular Component的Life Cycle(生命週期)
         * 觸發時機為創建好Component時,會呼叫一次
         * 有機會會針對生命週期做一次介紹
         * 在developer tool中可以看到的log為
         * 
         * input1 123
         * input3 456
         */
        other-page.html
        <yur-input input1="123" input2="456"></yur-input>
        .ts
        @Component({
            selector: 'yur-input',
            ...
        })
        export class InputComponent implements OnInit {
            @Input() input1;
            @Input('input2') input3;
            
            ngOnInit() {
                console.log('input1', this.input1);
                console.log('input3', this.input3);
            }
        }
        
    • @Output
      • 說明: 可以在Template中將事件綁定在Dom上,並可以自己或是使用者決定何時觸發該事件。
      • 範例:
        /*
         * 與Input相同,沒給metadata時
         * component上的屬性output1會照原本命名
         * 當有指定名稱時,如指定output2則代表對外給予的綁定事件input2
         * 但是在component內部時,參數名稱則是output3
         * EventEmitter是自定事件時的class,提供emit和subscribe
         * 前者類似於告訴別人我做了這件事,ex: a.emit(doSomething)
         * 後者是等待別人告訴我事件觸發了,ex: a.subscribe(waitToSomething)
         * 
         * 網頁顯示為
         *  -------------   -------------
         * |click output1| |click output3|
         *  -------------   -------------
         * 接著點擊click output1會變為
         * 
         * hello
         *  -------------   -------------
         * |click output1| |click output3|
         *  -------------   -------------
         * 接著點擊click output3會變為
         * 
         * hello
         * world
         *  -------------   -------------
         * |click output1| |click output3|
         *  -------------   -------------
         *  
         * 從這邊可以看出來,點擊click output1這個button就是觸發
         * 自定義的output1,同理output3也是如此,
         * 而textContent是Html Dom自有的屬性,我只是給予他文字
         */
        other-page.html
        <div #div1></div>
        <div #div2></div>
        <yur-output (output1)="div1.textContent = $event" (output2)="div2.textContent = $event"></yur-output>
        .ts
        @Component({
            selector: 'yur-output',
            template: `
                <button (click)="output1.emit('hello')">click output1</button>
                <button (click)="output3.emit('world')">click output3</button>
            `
        })
        export class InputComponent {
            @Output() output1 = new EventEmitter();
            @Output('output2') output3 = new EventEmitter();
        }
        
    • 如何做出像ngModel的雙向綁定
      • 其實只要改變一下命名方式,像這樣@Input() value@Output() valueChange,將想雙向綁定的Input參數名稱多個Change就可以達到雙向綁定了
      • 範例:
        /*
         * 以下先創建一個顯示two-way binding用的頁面(otherPage)
         * 並且給予他count預設值及要顯示的頁面
         * 接下來定義我們的屬性和事件,這邊名字如何都可以,但若要可以雙向綁定
         * 就需要在output後面對應input名稱加上Change
         * 這邊就是counterChange
         * 頁面顯示為
         * count: 0
         *  -------
         * | Click |
         *  -------
         *  
         * 點擊Click的按鈕後,頁面顯示為
         * count: 1
         *  -------
         * | Click |
         *  -------
         *  
         * html上看到的[(counter)]="count"
         * 就是我們一直在介紹的two-way binding :D
         */
        .other-page.component
        @Component({...})
        export class OtherPageComponent {
            count = 0;
        }
        .other-page.html
        <div>count: {{count}}</div>
        <yur-two-way-binding [(counter)]="count"></yur-two-way-binding>
        
        @Component({
            selector: 'yur-two-way-binding',
            template: `
                <button (click)="counterChange.emit(counter + 1)">Click</button>
            `
        })
        export class TwoWayBindingComponent {
            @Input() counter;
            @Output() counterChange = new EventEmitter();
        }
        
  • 以下為Angular官網提供的資料綁定示意圖

總結

以上大致把Angular比較基礎的東西補足,方便後續在說明例子時能有共識,那因為篇幅較長,只好把Note list的實作篇放在下一章節來做介紹,以上若有說明不洽當或是觀念錯誤的話,再麻煩大家告訴我哩,再次感謝大家耐心地閱讀:D

You Don't Know JS 小筆記 - 作用域

參考文章 You Don't Know JS: Scope & Closures - 1st Edition

關於作用域

前言

Javascript常被說是動態語言,但他其實是編譯型語言,因為他在執行前會經由分詞(Tokenizing)/詞法分析(lexing)解析(parsing)代碼生成(code-generation)這三個步驟,而在javascript code執行前都必須被編譯,通常發生在code剛要執行前,所以為了確保快速的性能,有JIT(Just In Time)、AOT(Ahead Of Time)。

簡單說明一下JIT跟AOT差別

  • AOT是預先編譯,一開始就先將程式碼編譯到瀏覽器認識的最佳化樣子,因此在瀏覽器載入時相對快速,檔案也相對小,更可以先對程式碼做一些語法上的錯誤判斷。
  • JIT是即時編譯,也就是在瀏覽器上載到哪編譯到哪,所以在解析過程還要優化程式碼,相對AOT來說就比較慢。

引擎與編譯器和作用域的關係

引擎包含編譯器與作用域
編譯器在編譯程式碼時,會檢查變數是否存在於作用域,不存在則請作用域聲明這個變量並存在表中;接著在生成的程式碼後,引擎執行程式碼並針對賦值的變數查詢當前作用域的表看是否有這個變數,有就使用,沒有就繼續查詢外層的的作用域,若最終沒查到就會拋出錯誤。

LHS & RHS

var a = 2;
當程式碼編譯時,會先詢問作用域a是否聲明a這個變數,這個詢問就是LHS引用,而在引擎執行程式時a = 2這個賦值行為將是引擎跟作用域查詢a是否存在,他要將a賦予2這個數值,這個查詢的動作就是RHS引用。

ReferenceError & TypeError

  • LHS 引用在作用域中找不到變數時,會為變數自動聲明為全域變數,但在嚴格模式時,會拋出ReferenceError
use 'strict'
a = 2; // ReferenceError
  • RHS引用在作用域中找不到變數時,會拋出ReferenceError
use 'strict'
console.log(a); // ReferenceError
  • RHS引用在作用域中找到了變數,但是變數使用了不是自己的方法時會拋出TypeError
var a = 1;
a.helloWorld(); // TypeError
  • 結論是ReferenceError為作用域解析失敗,而TypeError為作用域解析成功但是做出了非法的行為。

詞法作用域

詞法作用域是在編譯器將程式碼分詞與分詞分析時被定義的作用域,也就代表詞法作用域在一開始定義的時候就決定了,若在當前作用域找不到時從外層作用域第一個找到的就會優先選取。

// 詞法作用域為全域
function a() {
    // 詞法作用域為a
    function b() {
        // 詞法作用域為b
    }
} 
// 因為a一開始定義的外層詞法作用域為全域
function a() {
 console.log(zz);
}
var zz = 1;

function b() {
  var zz = 2;
  a();
}

b(); // 1

函數表達式與函數聲明式差別

// 函數表達式
var a = function a() {};
// 函數立即表達式
(function IIFE() {})();
// 函數聲明式
function a() {}

提升(Hoisting)

函數聲明與變數宣告會在編譯器進行分詞/詞法解析時丟到各自的作用域,函數聲明的提升優先權大於變數宣告,接著才是做賦值的執行。

// function a()被提升後,才輪到變數a被提升,但function已經命名過a,
// 所以var a的宣告被忽略,不過還是可以接受賦值
var a;
a(); // 1
function a() { console.log(1); }
a = 3;
console.log(a); // 3
// 兩個都被提升,第二個蓋過第一個
function a() { console.log(1); }
function a() { console.log(2); }
a(); // 2
// 兩個都被提升,但在執行時因為賦值,所以顯示console.log(1)
var a = function() { console.log(1); }
function a() { console.log(2); }
a(); // 1

閉包

閉包就是當一個函數不在他的詞法作用域調用,也依然能訪問到他的詞法作用域,在上面的例子中,其實就有用到閉包

// 因為a一開始定義的外層詞法作用域為全域
function a() {
 console.log(zz);
}
var zz = 1;

function b() {
  var zz = 2;
  a();// <-- 使用避包紀錄zz為1
}

b(); // 1

垃圾回收機制(Garbage Collector)

當某段程式碼沒有被執行時就會進行回收

function a(test) { console.log(test); }
var t = 'hello world';
a(t);
// 執行完a後,a的作用域消失被回收掉了

function b() {
	var g = 'trigger gc';
  function c() {
		console.log(g);
	}
	return c;
}

var d = b(); // b的作用域還沒被回收,因為c被回傳出來,所以還霸佔著內存,而這也是閉包的影響
d(); // 到這裡才會進行回收

You Don't Know JS 小筆記 - callback & promise & async

參考文章 You Don't Know JS: Async & Performance

關於callback

callback就是在函數執行完畢後,會執行的函數

function greeting(callback) {
	console.log('hello');
	callback();
}

greeting(function() {
	console.log('greeting completed')
});
// hello
// greeting completed

callback的缺點

  • 無法控制callback什麼時候執行,造成信任度低
  • 當過多的callback時,會因為程式碼跳來跳去造成可讀性差

無法控制callback什麼時候執行,造成信任度低

以吃飯邊看電視的例子來看,我希望吃東西時配電視看,可是因為沒辦法控制eatFinish什麼時候呼叫,所以變成吃飯 -> 吃飽 -> 看電視

function eat(callback) {
	console.log('Eating');
	callback();
}

function watchTv() {
	console.log('Watching TV');
}

eat(function eatFinish() {
	console.log('Eat Finish');
});
watchTv();
// Eating
// Eat Finish
// Watching TV

當過多的callback時,會因為程式碼跳來跳去造成可讀性差

這邊以煮泡麵打電腦完洗澡的例子來看,
我們煮開水 -> 煮泡麵 -> 玩遊戲 -> 關電腦 -> 洗澡,這個流程很正常,但是我們看以下程式碼時,是有點痛苦的,為什麼呢,因為每個動作都黏在一起,煮開水,去煮泡麵,可是要煮完泡麵才能玩遊戲,所以也放放在同一塊,而且強迫看code的人需要一個一個細心理解每行程式碼做到哪要做哪一步驟,這都會消耗蠻多心力,這種寫法也叫做callback hell

function boilWater(callback) {
	console.log('Boil Water');
	callback();
}

function playGame(callback) {
	console.log('Playing Game');
	callback();
}

function  takeTheBath() {
	console.log('Take the Bath');
}

function action() {
	boilWater(function cookInstantNoddles() {
		console.log('Cook Instant Noddles');
		playGame(function turnOffComputer() {
			console.log('Turn Off Computer');
			takeTheBath();
		});
	});
}

action();

關於promise

隨著時代演進,為了改善callback的問題,創造了Promise,以下用段故事說明Promise的行為

我走到肯德基櫃檯,點了雞米花,我藉由付款的行為(request),得到了叫號牌587,也就是等等會給我雞米花的承諾(Promise)

於是我愜意的拿著叫號牌去附近的書店晃晃看看書,看著看著覺得肚子餓了,就回到肯德基的櫃檯,也不管是不是過號了,跟店員說著,我的叫號牌587好了嘛,好了就給我雞米花吧,這時會有兩個常見的場景跟一個不常見的場景出現
- 常見
1. 店員雖然覺得我很慢才來拿,但看到叫號牌,還是拿了餐點給我 (成功)
2. 店員跟我道歉,原本以為還有雞米花,結果點完餐後才發現沒了 (失敗)
- 不常見
3. 我回去時,店員說電腦上沒有587的資料,是不是我走錯地方了 (無回應)

總得來說,至少在常見的情況下,我可以隨時掌控我何時要拿雞米花這件事,跟callback相比,callback就像是郵差一樣,我寄了信,他何時送達,有沒有送達成功,都可能需要第三個人跟我說,我根本沒辦法掌握我那封信的成功與否。

Promise改善了什麼

讓我們先回顧一下callback的缺點

  • 無法控制callback什麼時候執行,造成信任度低
    • Promise藉由上則故事可以知道,他將控制權反轉過來抓在手裡,且在常見的情況下,可以知道是成功還是失敗,並且只會是成功或失敗其中一中,不會兩個同時並存。
function checkWallet() { ... }
const order = new Promise((resolve, reject) => {
	if (checkWallet()) {
		resolve('付款成功');
	} else {
		reject('錢不夠');
	}
});

order
.then(() => {
	console.log('好吃'); // checkWallet回傳true就顯示好吃
})
.catch(() => {
	console.log('餓肚子') // checkWallet回傳false就顯示餓肚子
})
  • 當過多的callback時,會因為程式碼跳來跳去造成可讀性差
    • 在大部分的情況下promise的巢狀會比較少一點,有加強了可讀性,但不可否認,在一些情況下,並不會改善什麼。
// 這邊一樣以`煮開水` -> `煮泡麵` -> `玩遊戲` -> `關電腦` -> `洗澡`為例子
function boilWater() {
	console.log('Boil Water');
	return Promise.resolve();
}

function cookInstantNoddles() {
	console.log('Cook Instant Noddles');
  return Promise.resolve();
});

function playGame(callback) {
	console.log('Playing Game');
	return Promise.resolve();
}

function turnOffComputer() {
	console.log('Turn Off Computer');
	return Promise.resolve();
});

function  takeTheBath() {
	console.log('Take the Bath');
	return Promise.resolve();
}

function action() {
	// 是不是簡潔閱讀了許多了呢?
	boilWater()
	.then(() => cookInstantNoddles())
	.then(() => playGame())
	.then(() => turnOffComputer())
	.then(() => takeTheBath());
}

action();

關於async & await

隨著時代演進,callback的大部分缺點都被promise所改善了,但是當邏輯相當複雜時,promise的巢狀還是有那麼點閱讀上的缺憾,沒錯,async, await幫助我們解決了這個問題

async&await如何改善閱讀上的問題

我們將剛剛的煮泡麵再複雜化一點
煮開水 -> 拿兩顆蛋 -> 同時煮泡麵和水煮蛋 -> 加蛋到泡麵鍋和水煮蛋鍋裡 -> 加辣 -> 試吃 -> 加辣

function boilWater() {
	console.log('Boil Water');
	return Promise.resolve();
}

function takeEggs(count) {
	const eggs = [];
	for (let i = 0; i < count ; i++) {
		eggs.push('egg');
	}
	return eggs;
}

function cookInstantNoddles() {
	console.log('Cook Instant Noddles');
  return Promise.resolve();
});

function cookSoftBoiledEggs() {
	console.log('Cook Soft-Boiled Eggs');
  return Promise.resolve();
}

function addItemTo(item, where) {
	console.log(`Add ${item} to ${where}`);
	return Promise.resolve();
}

boilWater()
.then(() => {
	const eggs = takeEggs(2);
	const p1 = cookInstantNoddles()
	.then(() => addItemTo(eggs[0], 'first pot'));

	const p2 = cookSoftBoiledEggs()
	.then(() => addItemTo(eggs[1], 'second pot'));
	return Promise.all([p1, p2])
});

是不是有越來越複雜的傾向了呢,接著我們用async, await試著做一遍,是不是看起來順利的把code扁平化了,而且一路看下來不需要兜圈子了呢

try {
	await boilWater();
	const eggs = takeEggs(2);
	await cookInstantNoddles();
	await cookSoftBoiledEggs();
	await addItemTo(eggs[0], 'first pot');
	await addItemTo(eggs[1], 'second pot');
} catch(err) {
	console.log(err.msg);
}

Angular小教室 - 如何建置專案

Angular小教室 - 如何建置專案

我們在開發專案時,通常都要重新建置檔案目錄,初始化要用的套件還有一些打包工具,那Angular這套框架是有提供CLI(Command Line Interface)來讓我們輸入指令就可以快速的搭建起專案,專注在開發上,接下來我們就來一步一步建立屬於自己的專案吧。

環境設定

那到底我們要怎麼下載angular cli呢,步驟如下

  • 下載nodejs,若已下載可以跳過此步驟

    • 為什麼要下載nodejs呢,因為它提供了一個叫做npm(node package manager)的線上套件管理資料庫,下載完後就可以藉由npm去下載人家打包好的套件來使用啦。
    • 使用方式就是npm install [name]
  • 打開終端機(terminal)或是命令提示字元(cmd)並輸入以下指令

    npm install -g @angular/cli
    // 下載angular cli到系統上,-g是global代表系統上都可以訪問到他
    
  • 測試一下是否安裝成功,輸入下列指令

    ng version // 觀看angular cli版本
    
    -----------應該會出現以下畫面----------
    Package                      Version
    ------------------------------------
    @angular-devkit/architect    0.13.5
    @angular-devkit/core         7.3.5
    @angular-devkit/schematics   7.3.5
    @schematics/angular          7.3.5
    @schematics/update           0.13.5
    rxjs                         6.3.3
    typescript                   3.2.4
    
  • 若環境設定有問題,可以參考一下大神寫的文章Angular 7 開發環境說明

建置專案

創建一個專案只要下這樣的指令就可以達成,不用自己創一堆檔案,一行瞬間幫你搞定XD
ng new [project name]

那像我本身會在創建時,希望CLI可以先幫我把一些東西建好,所以會多下一些指令給他,就像是下面這個指令

ng new angular-classroom --style=scss --prefix=yur --routing

/* ng new angular-classroom => 創建一個叫angular-classroom的專案
 * --style=scss => 使用Css預處理 - scss
 * --prefix=yur => 我的元件名稱前面的冠名是yur,預設是app
 * --rounting => 一開始就先幫我設定好我的路由模組
 */

到這邊就是簡單的專案建置流程,專案建立完成後,到Terminal切到我們的專案目錄cd angular-classroom運行npm start,執行完成後預設的網址應該為http://localhost:4200,這邊是今日文章的程式碼DEMO網站
那下面是一些常用的指令介紹,有興趣的朋友可以參考參考

  • 常用的指令介紹
    • new

      • Angular官網new介紹
      • 創建專案的指令
      • 用法:ng new [project name]
      • options
        • --style
          • 用來設定css的樣式
          • 用法:--style=[stylesheet]
        • --prefix
          • 用來設定元件的前綴詞
          • 用法:--prefix=[prefix name]
        • --routing
          • 開始就先創建好路由模組
          • 用法:--routing
        • --directory
          • 設定專案位置
          • 用法:--directory [folder position]
    • generate

      • Angular官網generate介紹
      • 縮寫g
      • 在已有專案中創建其他元件的指令
      • 用法: ng generate [element type] or ng g [element type]
      • element type
        • component
          • 縮寫c
          • 說明:創建元件,建構頁面的基礎。
          • 用法:ng g component [component name]ng g c [component name]
          • 需要指定路徑就ng g c [path]/[component name]
        • service
          • 縮寫s
          • 說明:創建服務,通常只提供一種類型的服務,如登入、驗證等服務
          • 用法:ng g service [service name]ng g s [service name]
          • 需要指定路徑就ng g s [path]/[service name]
        • pipe
          • 縮寫p
          • 說明:創建通道,通常用來將A轉換為B,如提供60秒經由通道轉換為1分鐘
          • 用法:ng g pipe [pipe name]ng g p [pipe name]
          • 需要指定路徑就ng g p [path]/[pipe name]
        • directive
          • 縮寫d
          • 說明:創建指令,可以賦予標籤或元件擁有某一種功能
          • 用法:ng g directive [directive name]ng g d [directive name]
          • 需要指定路徑就ng g d [path]/[directive name]
        • module
          • 縮寫m
          • 說明:創建模組,模組通常由功能特性劃分,如路由模組、驗證模組、報表模組,模組可以包含元件、服務、只是、通道及其他模組
          • 用法:ng g module [module name]ng g m [module name]
          • 需要指定路徑就ng g m [path]/[module name]
        • application
          • 說明:創建子專案,可以單獨當作一個app,也可以在主專案把子專案導入使用
          • 用法:ng g application [application name]
          • 創建位置預設在projects
    • serve

      • Angular官網serve介紹
      • 啟動專案的指令
      • 用法:ng serve 或開啟某個子專案ng serve [project name]
      • options
        • --aot
          • 全名:Ahead of time
          • 使用預先編譯模式啟動
          • 說明:因js在瀏覽器讀取時是採用JIT(Just-In-Time)的即時編譯模式,也就是每讀一次檔案就進行一次編譯,那AOT就是部署檔案前就預先進行編譯,那瀏覽器在讀取時就可以少掉編譯這邊的時間,增加使用者體驗啦,不過因為事先編譯,檔案可能會比沒用AOT的大些
          • 用法:ng serve --aot
        • --prod
          • 使用產品模式啟動
          • 說明:
            • 減少檔案大小:將程式碼的空白,註解清除,刪除未使用的方法(dead code elimination),import或export的method有使用到的才打包(tree-shaking)。
            • 優化速度:預設使用AOT模式
            • 縮小檔案數量:將使用到的library, js, css等檔案,打包起來。
          • 用法:ng serve --prod
        • --port
          • 更改啟動時的port
          • 說明:原先預設為4200,若要更改port可以使用這個指令
          • 用法:ng serve --port=4100
        • --sourceMap
          • 預設為開啟,建立對應編譯後的程式碼原始位置檔案
          • 說明: 因對應編譯後的程式碼位置,所以在除錯時可以看到原始的程式碼樣子,方便除錯
          • 用法: ng serve --sourceMap
        • --configuration
          • 載入angular.json中指定的名稱的configuration的設定
          • 說明: 原先是直接在terminal上直接打指令,這邊是改成載入預設好的物件來做設定,那更詳細的部分會在導入多語系實作說明。
          • 用法: ng serve --configuration=[angular.json setting config]ng serve -c=[angular.json setting config]
    • build

      • Angular官網build介紹
      • 啟動專案的指令
      • 用法:ng build 或開啟某個子專案ng build [project name]
      • options
        • 常用的與serve相似,所以就不多贅述啦XD

若想暸解cli還有什麼功能的話可以參考Angular官網Angular cli的Github,或是大神寫的Angular CLI 操作筆記,若說明中有什麼錯誤也歡迎指正:)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.