In JavaScript
In JavaScript
Variables
使用有意義且具備可讀性的名稱
const yyyymmdstr = moment().format('YYYY/MM/DD');
const yyyymmdstr = moment().format('YYYY/MM/DD');
const currentDate = moment().format('YYYY/MM/DD');
const currentDate = moment().format('YYYY/MM/DD');
相同類型的變數使用相同的名稱
getUserInfo(); getClientData(); getCustomerRecord();
getUserInfo(); getClientData(); getCustomerRecord();
getUser();
getUser();
使用可搜尋的名稱
// 86400000 代表什麼意義? setTimeout(blastOff, 86400000);
// 86400000 代表什麼意義? setTimeout(blastOff, 86400000);
// 宣告(Declare)一個有意義的常數(constants) const MILLISECONDS_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
// 宣告(Declare)一個有意義的常數(constants) const MILLISECONDS_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用可解釋的變數
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode( address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2] );
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode( address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2] );
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [, city, zipCode] = address.match(cityZipCodeRegex) || []; saveCityZipCode(city, zipCode);
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [, city, zipCode] = address.match(cityZipCodeRegex) || []; saveCityZipCode(city, zipCode);
避免心理作用(Mental Mapping)
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach(l => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 等等,這 `l` 是…? dispatch(l); });
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach(l => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 等等,這 `l` 是…? dispatch(l); });
避免心理作用(Mental Mapping)
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach(location => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach(location => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });
避免使用不必要的描述(Context)
const Car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; }
const Car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; }
避免使用不必要的描述(Context)
const Car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; }
const Car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; }
使用預設參數代替條件判斷
function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... }
function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... }
function createMicrobrewery(name = 'Hipster Brew Co.') { // ... }
function createMicrobrewery(name = 'Hipster Brew Co.') { // ... }
Functions
一個至二個是最理想的,儘可能避免大於三個以上。如果你有超過兩個以上的參數,代表你的函數做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。
為了清晰地表達預期使用哪些物件的屬性,可以使用解耦語法,優點如下:
function createMenu(title, body, buttonText, cancellable) { // ... }
function createMenu(title, body, buttonText, cancellable) { // ... }
function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true });
function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true });
// 結合上一章提過的的預設參數,可以改寫如下: function createMenu({ title = 'Default Title', // 傳遞的物件不齊全,使用預設屬性 body = '', buttonText = 'My Button', cancellable = true } = {}) { // 如未傳遞任何參數使用預設空物件,可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'. return { title, body, buttonText, cancellable } } const myMenu = createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true });
// 結合上一章提過的的預設參數,可以改寫如下: function createMenu({ title = 'Default Title', // 傳遞的物件不齊全,使用預設屬性 body = '', buttonText = 'My Button', cancellable = true } = {}) { // 如未傳遞任何參數使用預設空物件,可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'. return { title, body, buttonText, cancellable } } const myMenu = createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true });
function emailClients(clients) { clients.forEach(client => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
function emailClients(clients) { clients.forEach(client => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); } function emailActiveClients(clients) { clients.filter(isActiveClient).forEach(email); }
function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); } function emailActiveClients(clients) { clients.filter(isActiveClient).forEach(email); }
function addToDate(date, month) { // ... } const date = new Date(); // 難以從函數名稱看出到底加入了什麼 addToDate(date, 1);
function addToDate(date, month) { // ... } const date = new Date(); // 難以從函數名稱看出到底加入了什麼 addToDate(date, 1);
function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);
function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);
function showDeveloperList(developers) { developers.forEach(developer => { const expectedSalary = developer.calculateExpectedSalary(); const experience = developer.getExperience(); const githubLink = developer.getGithubLink(); const data = { expectedSalary, experience, githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { const expectedSalary = manager.calculateExpectedSalary(); const experience = manager.getExperience(); const portfolio = manager.getMBAProjects(); const data = { expectedSalary, experience, portfolio }; render(data); }); }
function showDeveloperList(developers) { developers.forEach(developer => { const expectedSalary = developer.calculateExpectedSalary(); const experience = developer.getExperience(); const githubLink = developer.getGithubLink(); const data = { expectedSalary, experience, githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { const expectedSalary = manager.calculateExpectedSalary(); const experience = manager.getExperience(); const portfolio = manager.getMBAProjects(); const data = { expectedSalary, experience, portfolio }; render(data); }); }
function showEmployeeList(employees) { employees.forEach(employee => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const data = { expectedSalary, experience }; switch (employee.type) { case 'manager': data.portfolio = employee.getMBAProjects(); break; case 'developer': data.githubLink = employee.getGithubLink(); break; } render(data); }); }
function showEmployeeList(employees) { employees.forEach(employee => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const data = { expectedSalary, experience }; switch (employee.type) { case 'manager': data.portfolio = employee.getMBAProjects(); break; case 'developer': data.githubLink = employee.getGithubLink(); break; } render(data); }); }
重用性固然重要,但潛在代價就是:
const menuConfig = { title: null, body: 'Bar', buttonText: null, cancellable: true }; function createMenu(config) { config.title = config.title || 'Foo'; config.body = config.body || 'Bar'; config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable !== undefined ? config.cancellable : true; } createMenu(menuConfig);
const menuConfig = { title: null, body: 'Bar', buttonText: null, cancellable: true }; function createMenu(config) { config.title = config.title || 'Foo'; config.body = config.body || 'Bar'; config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable !== undefined ? config.cancellable : true; } createMenu(menuConfig);
const menuConfig = { title: 'Order', // 漏掉 body buttonText: 'Send', cancellable: true }; function createMenu(config) { config = Object.assign( { title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config ); // config 現在等同於: { title: 'Order', body: 'Bar', buttonText: 'Send', cancellable: true } } createMenu(menuConfig);
const menuConfig = { title: 'Order', // 漏掉 body buttonText: 'Send', cancellable: true }; function createMenu(config) { config = Object.assign( { title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config ); // config 現在等同於: { title: 'Order', body: 'Bar', buttonText: 'Send', cancellable: true } } createMenu(menuConfig);
簡言之就是不要立 flag
function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }
function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }
function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); }
function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); }
let name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];
let name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];
function splitIntoFirstAndLastName(name) { return name.split(' '); } const name = 'Ryan McDermott'; const newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];
function splitIntoFirstAndLastName(name) { return name.split(' '); } const name = 'Ryan McDermott'; const newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];
const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); };
const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); };
const addItemToCart = (cart, item) => { return [ ...cart, { item, date: Date.now() } ]; };
const addItemToCart = (cart, item) => { return [ ...cart, { item, date: Date.now() } ]; };
Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); };
Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); };
class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } }
class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } }
const programmerOutput = [ { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; // 不必要的變數 let totalOutput = 0; for (let i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }
const programmerOutput = [ { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; // 不必要的變數 let totalOutput = 0; for (let i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }
const programmerOutput = [ { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; const totalOutput = programmerOutput.reduce( (totalLines, output) => totalLines + output.linesOfCode, 0 );
const programmerOutput = [ { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; const totalOutput = programmerOutput.reduce( (totalLines, output) => totalLines + output.linesOfCode, 0 );
function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... }
function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... }
function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... }
function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... }
有很多方法可以避免,像是統一所有的 API
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.pedal(this.currentLocation, new Location('texas')); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location('texas')); } }
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.pedal(this.currentLocation, new Location('texas')); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location('texas')); } }
// 統一了所有的車輛移動的參數、方法與實作 // 所以不再需要區分不同的類別的車輛呼叫不同的方法 function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location('texas')); }
// 統一了所有的車輛移動的參數、方法與實作 // 所以不再需要區分不同的類別的車輛呼叫不同的方法 function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location('texas')); }
function combine(val1, val2) { if ( (typeof val1 === 'number' && typeof val2 === 'number') || (typeof val1 === 'string' && typeof val2 === 'string') ) { return val1 + val2; } throw new Error('Must be of type String or Number'); }
function combine(val1, val2) { if ( (typeof val1 === 'number' && typeof val2 === 'number') || (typeof val1 === 'string' && typeof val2 === 'string') ) { return val1 + val2; } throw new Error('Must be of type String or Number'); }
function combine(val1, val2) { return val1 + val2; }
function combine(val1, val2) { return val1 + val2; }
手動型別檢查需要很多額外處理,你得到的可能是虛假的型別檢查且失去可讀性,所以:
程式語言效能會因版本更新而得到優化,但不要因此放棄所有的優化,演算法的優化才是該注意的地方
// 在舊的瀏覽器中並不會快取 list.length,每次 iteration 時的重新計算相當損耗效能 // 這在新瀏覽器中已被優化,所以不用再手動去快取 for (let i = 0, len = list.length; i < len; i++) { // ... }
// 在舊的瀏覽器中並不會快取 list.length,每次 iteration 時的重新計算相當損耗效能 // 這在新瀏覽器中已被優化,所以不用再手動去快取 for (let i = 0, len = list.length; i < len; i++) { // ... }
for (let i = 0; i < list.length; i++) { // ... }
for (let i = 0; i < list.length; i++) { // ... }
Objects & Data Structure
比單純使用屬性(property)來的好,因為:
function makeBankAccount() { // ... return { balance: 0 // ... }; } const account = makeBankAccount(); account.balance = 100;
function makeBankAccount() { // ... return { balance: 0 // ... }; } const account = makeBankAccount(); account.balance = 100;
function makeBankAccount() { // 私有變數 let balance = 0; // 'getter',經由下方的返回物件對外公開 function getBalance() { return balance; } // 'setter',經由下方的返回物件對外公開 function setBalance(amount) { // ... 更新前先進行驗證 balance = amount; } return { // ... getBalance, setBalance }; } const account = makeBankAccount(); account.setBalance(100);
function makeBankAccount() { // 私有變數 let balance = 0; // 'getter',經由下方的返回物件對外公開 function getBalance() { return balance; } // 'setter',經由下方的返回物件對外公開 function setBalance(amount) { // ... 更新前先進行驗證 balance = amount; } return { // ... getBalance, setBalance }; } const account = makeBankAccount(); account.setBalance(100);
可以透過閉包(closures)來私有化參數
const Employee = function(name) { this.name = name; }; Employee.prototype.getName = function getName() { return this.name; }; const employee = new Employee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
const Employee = function(name) { this.name = name; }; Employee.prototype.getName = function getName() { return this.name; }; const employee = new Employee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
可以透過閉包(closures)來私有化參數
function makeEmployee(name) { return { getName() { return name; } }; } const employee = makeEmployee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
function makeEmployee(name) { return { getName() { return name; } }; } const employee = makeEmployee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
Classes
這個 pattern 在 JavaScript 中非常有用,可以在很多函數庫中看到,像是 jQuery 與 Lodash。它可以讓你的程式碼表達的更好:
class Car { constructor(model, color) { this.model = model; this.color = color; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.model, this.color); } } const car = new Car('F-150', 'red'); car.setColor('pink'); car.save();
class Car { constructor(model, color) { this.model = model; this.color = color; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.model, this.color); } } const car = new Car('F-150', 'red'); car.setColor('pink'); car.save();
class Car { constructor(model, color) { this.model = model; this.color = color; } setModel(model) { this.model = model; return this; } setColor(color) { this.color = color; return this; } save() { console.log(this.model, this.color); return this; } } const car = new Car('F-150', 'red') .setColor('pink') .save();
class Car { constructor(model, color) { this.model = model; this.color = color; } setModel(model) { this.model = model; return this; } setColor(color) { this.color = color; return this; } save() { console.log(this.model, this.color); return this; } } const car = new Car('F-150', 'red') .setColor('pink') .save();
說明一下什麼時候使用繼承比組合更合適:
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // 因為員工有稅率金資料,而不是某一種員工 class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // 因為員工有稅率金資料,而不是某一種員工 class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... } class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } // ... }
class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... } class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } // ... }
SOLID
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } }
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } }
class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }
class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { if (this.adapter.name === 'ajaxAdapter') { return makeAjaxCall(url).then(response => { // 轉換回應並回傳 }); } else if (this.adapter.name === 'nodeAdapter') { return makeHttpCall(url).then(response => { // 轉換回應並回傳 }); } } } function makeAjaxCall(url) { // 發送請求並回傳 promise } function makeHttpCall(url) { // 發送請求並回傳 promise }
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { if (this.adapter.name === 'ajaxAdapter') { return makeAjaxCall(url).then(response => { // 轉換回應並回傳 }); } else if (this.adapter.name === 'nodeAdapter') { return makeHttpCall(url).then(response => { // 轉換回應並回傳 }); } } } function makeAjaxCall(url) { // 發送請求並回傳 promise } function makeHttpCall(url) { // 發送請求並回傳 promise }
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } request(url) { // 發送請求並回傳 promise } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } request(url) { // 發送請求並回傳 promise } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then(response => { // transform response and return }); } }
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } request(url) { // 發送請求並回傳 promise } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } request(url) { // 發送請求並回傳 promise } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then(response => { // transform response and return }); } }
正式的定義為:
class Rectangle { constructor() { this.width = 0; this.height = 0; } setColor(color) { // ... } render(area) { // ... } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Rectangle { setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } } function renderLargeRectangles(rectangles) { rectangles.forEach(rectangle => { rectangle.setWidth(4); rectangle.setHeight(5); const area = rectangle.getArea(); rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
class Rectangle { constructor() { this.width = 0; this.height = 0; } setColor(color) { // ... } render(area) { // ... } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Rectangle { setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } } function renderLargeRectangles(rectangles) { rectangles.forEach(rectangle => { rectangle.setWidth(4); rectangle.setHeight(5); const area = rectangle.getArea(); rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
class Shape { setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor(length) { super(); this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach(shape => { const area = shape.getArea(); shape.render(area); }); } const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeShapes(shapes);
class Shape { setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor(length) { super(); this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach(shape => { const area = shape.getArea(); shape.render(area); }); } const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeShapes(shapes);
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), animationModule() {} // 大多數的情況下,執行 traverse 時,我們其實不需要使用 animation。 // ... });
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), animationModule() {} // 大多數的情況下,執行 traverse 時,我們其實不需要使用 animation。 // ... });
class DOMTraverser { constructor(settings) { this.settings = settings; this.options = settings.options; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.setupOptions(); } setupOptions() { if (this.options.animationModule) { // ... } } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), options: { // animationModule 可被選擇要不要使用 animationModule() {} } });
class DOMTraverser { constructor(settings) { this.settings = settings; this.options = settings.options; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.setupOptions(); } setupOptions() { if (this.options.animationModule) { // ... } } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), options: { // animationModule 可被選擇要不要使用 animationModule() {} } });
/* 在糟糕的案例中,InventoryTracker 沒有提供替換請求模組的可能, InventoryTracker 依賴於 InventoryRequester,造成耦合, 測試將會被難以撰寫,修改程式碼時將會牽一髮而動全身 */ class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryTracker { constructor(items) { this.items = items; // 糟糕的:我們建立了一種依賴,依賴於特定(InventoryRequester)請求模組的實現。 // 我們實際上只有 requestItem 方法依賴於 request 這個方法。 this.requester = new InventoryRequester(); } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } const inventoryTracker = new InventoryTracker(['apples', 'bananas']); inventoryTracker.requestItems();
/* 在糟糕的案例中,InventoryTracker 沒有提供替換請求模組的可能, InventoryTracker 依賴於 InventoryRequester,造成耦合, 測試將會被難以撰寫,修改程式碼時將會牽一髮而動全身 */ class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryTracker { constructor(items) { this.items = items; // 糟糕的:我們建立了一種依賴,依賴於特定(InventoryRequester)請求模組的實現。 // 我們實際上只有 requestItem 方法依賴於 request 這個方法。 this.requester = new InventoryRequester(); } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } const inventoryTracker = new InventoryTracker(['apples', 'bananas']); inventoryTracker.requestItems();
class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ['WS']; } requestItem(item) { // ... } } // 通過外部創建時將依賴注入 // 我們可以輕鬆地用全新的 WebSockets 請求模組替換 const inventoryTracker = new InventoryTracker( ['apples', 'bananas'], new InventoryRequesterV2() ); inventoryTracker.requestItems();
class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ['WS']; } requestItem(item) { // ... } } // 通過外部創建時將依賴注入 // 我們可以輕鬆地用全新的 WebSockets 請求模組替換 const inventoryTracker = new InventoryTracker( ['apples', 'bananas'], new InventoryRequesterV2() ); inventoryTracker.requestItems();
Testing
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles date boundaries', () => { let date; date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles date boundaries', () => { let date; date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles 30-day months', () => { const date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); }); it('handles leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles 30-day months', () => { const date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); }); it('handles leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
Concurrency
import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then(response => { return writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch(err => { console.error(err); });
import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then(response => { return writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch(err => { console.error(err); });
// Async/Await 比 Promises 更加簡潔 import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; async function getCleanCodeArticle() { try { const response = await get( 'https://en.wikipedia.org/wiki/Robert_Cecil_Martin' ); await writeFile('article.html', response); console.log('File written'); } catch (err) { console.error(err); } }
// Async/Await 比 Promises 更加簡潔 import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; async function getCleanCodeArticle() { try { const response = await get( 'https://en.wikipedia.org/wiki/Robert_Cecil_Martin' ); await writeFile('article.html', response); console.log('File written'); } catch (err) { console.error(err); } }
Error Handling
try { functionThatMightThrow(); } catch (error) { console.log(error); }
try { functionThatMightThrow(); } catch (error) { console.log(error); }
try { functionThatMightThrow(); } catch (error) { // 可以這樣 console.error(error); // 或這種方法 notifyUserOfError(error); // 另外一種方法 reportErrorToService(error); // 或是全部都做 }
try { functionThatMightThrow(); } catch (error) { // 可以這樣 console.error(error); // 或這種方法 notifyUserOfError(error); // 另外一種方法 reportErrorToService(error); // 或是全部都做 }
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { console.log(error); });
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { console.log(error); });
// 不要忽略被拒絕的 Promises getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { // 可以這樣(會比 console.log 更吵) console.error(error); // 或這種方法 notifyUserOfError(error); // 另外一種方法 reportErrorToService(error); // 或是全部都做! });
// 不要忽略被拒絕的 Promises getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { // 可以這樣(會比 console.log 更吵) console.error(error); // 或這種方法 notifyUserOfError(error); // 另外一種方法 reportErrorToService(error); // 或是全部都做! });
Comments
多數情況下,好的程式碼本身就是文件
function hashIt(data) { // The hash let hash = 0; // Length of string const length = data.length; // Loop through every character in data for (let i = 0; i < length; i++) { // Get character code. const char = data.charCodeAt(i); // Make the hash hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; } }
function hashIt(data) { // The hash let hash = 0; // Length of string const length = data.length; // Loop through every character in data for (let i = 0; i < length; i++) { // Get character code. const char = data.charCodeAt(i); // Make the hash hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; } }
多數情況下,好的程式碼本身就是文件
function hashIt(data) { let hash = 0; const length = data.length; for (let i = 0; i < length; i++) { const char = data.charCodeAt(i); hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; } }
function hashIt(data) { let hash = 0; const length = data.length; for (let i = 0; i < length; i++) { const char = data.charCodeAt(i); hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; } }
用 Git 管理,舊的程式碼留在歷史紀錄中就好
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
doStuff();
doStuff();
不需要無用的、註解掉的程式碼,尤其是日誌式的註解
doStuff(); /** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */ function combine(a, b) { return a + b; }
doStuff(); /** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */ function combine(a, b) { return a + b; }
function combine(a, b) { return a + b; }
function combine(a, b) { return a + b; }