如何寫出優(yōu)雅耐看的JavaScript代碼
前言
在我們平時(shí)的工作開發(fā)中,大多數(shù)都是大人協(xié)同開發(fā)的公共項(xiàng)目;在我們平時(shí)開發(fā)中代碼codeing的時(shí)候我們考慮代碼的可讀性、復(fù)用性和擴(kuò)展性。
干凈的代碼,既在質(zhì)量上較為可靠,也為后期維護(hù)、升級奠定了良好基礎(chǔ)。
我們從以下幾個(gè)方面進(jìn)行探討:
變量
1、變量命名
一般我們在定義變量是要使用有意義的詞匯命令,要做到見面知義
- //bad code
- const yyyymmdstr = moment().format('YYYY/MM/DD');
- //better code
- const currentDate = moment().format('YYYY/MM/DD');
2、可描述
通過一個(gè)變量生成了一個(gè)新變量,也需要為這個(gè)新變量命名,也就是說每個(gè)變量當(dāng)你看到他第一眼你就知道他是干什么的。
- //bad code
- const ADDRESS = 'One Infinite Loop, Cupertino 95014';
- const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
- saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]);
- //better code
- const ADDRESS = 'One Infinite Loop, Cupertino 95014';
- const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
- const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || [];
- saveCityZipCode(city, zipCode);
3、形參命名
在for、forEach、map的循環(huán)中我們在命名時(shí)要直接
- //bad code
- const locations = ['Austin', 'New York', 'San Francisco'];
- locations.map((l) => {
- doStuff();
- doSomeOtherStuff();
- // ...
- // ...
- // ...
- // 需要看其他代碼才能確定 'l' 是干什么的。
- dispatch(l);
- });
- //better code
- const locations = ['Austin', 'New York', 'San Francisco'];
- locations.forEach((location) => {
- doStuff();
- doSomeOtherStuff();
- // ...
- // ...
- // ...
- dispatch(location);
- });
4、避免無意義的前綴
例如我們只創(chuàng)建一個(gè)對象是,沒有必要再把每個(gè)對象的屬性上再加上對象名
- //bad code
- const car = {
- carMake: 'Honda',
- carModel: 'Accord',
- carColor: 'Blue'
- };
- function paintCar(car) {
- car.carColor = 'Red';
- }
- //better code
- const car = {
- make: 'Honda',
- model: 'Accord',
- color: 'Blue'
- };
- function paintCar(car) {
- car.color = 'Red';
- }
5、默認(rèn)值
- //bad code
- function createMicrobrewery(name) {
- const breweryName = name || 'Hipster Brew Co.';
- // ...
- }
- //better code
- function createMicrobrewery(name = 'Hipster Brew Co.') {
- // ...
- }
函數(shù)
1、參數(shù)
一般參數(shù)多的話要使用ES6的解構(gòu)傳參的方式
- //bad code
- function createMenu(title, body, buttonText, cancellable) {
- // ...
- }
- //better code
- function createMenu({ title, body, buttonText, cancellable }) {
- // ...
- }
- //better code
- createMenu({
- title: 'Foo',
- body: 'Bar',
- buttonText: 'Baz',
- cancellable: true
- });
2、單一化處理
一個(gè)方法里面最好只做一件事,不要過多的處理,這樣代碼的可讀性非常高
- //bad code
- function emailClients(clients) {
- clients.forEach((client) => {
- const clientRecord = database.lookup(client);
- if (clientRecord.isActive()) {
- email(client);
- }
- });
- }
- //better code
- function emailActiveClients(clients) {
- clients
- .filter(isActiveClient)
- .forEach(email);
- }
- function isActiveClient(client) {
- const clientRecord = database.lookup(client);
- return clientRecord.isActive();
- }
3、對象設(shè)置默認(rèn)屬性
- //bad code
- 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);
- //better code
- const menuConfig = {
- title: 'Order',
- // 'body' key 缺失
- buttonText: 'Send',
- cancellable: true
- };
4、避免副作用
函數(shù)接收一個(gè)值返回一個(gè)新值,除此之外的行為我們都稱之為副作用,比如修改全局變量、對文件進(jìn)行 IO 操作等。
當(dāng)函數(shù)確實(shí)需要副作用時(shí),比如對文件進(jìn)行 IO 操作時(shí),請不要用多個(gè)函數(shù)/類進(jìn)行文件操作,有且僅用一個(gè)函數(shù)/類來處理。也就是說副作用需要在唯一的地方處理。
副作用的三大天坑:隨意修改可變數(shù)據(jù)類型、隨意分享沒有數(shù)據(jù)結(jié)構(gòu)的狀態(tài)、沒有在統(tǒng)一地方處理副作用。
- //bad code
- // 全局變量被一個(gè)函數(shù)引用
- // 現(xiàn)在這個(gè)變量從字符串變成了數(shù)組,如果有其他的函數(shù)引用,會(huì)發(fā)生無法預(yù)見的錯(cuò)誤。
- var name = 'Ryan McDermott';
- function splitIntoFirstAndLastName() {
- name = name.split(' ');
- }
- splitIntoFirstAndLastName();
- console.log(name); // ['Ryan', 'McDermott'];
- //better code
- var name = 'Ryan McDermott';
- var newName = splitIntoFirstAndLastName(name)
- function splitIntoFirstAndLastName(name) {
- return name.split(' ');
- }
- console.log(name); // 'Ryan McDermott';
- console.log(newName); // ['Ryan', 'McDermott'];
在 JavaScript 中,基本類型通過賦值傳遞,對象和數(shù)組通過引用傳遞。以引用傳遞為例:
假如我們寫一個(gè)購物車,通過 addItemToCart()方法添加商品到購物車,修改 購物車數(shù)組。此時(shí)調(diào)用 purchase()方法購買,由于引用傳遞,獲取的 購物車數(shù)組正好是最新的數(shù)據(jù)。
看起來沒問題對不對?
如果當(dāng)用戶點(diǎn)擊購買時(shí),網(wǎng)絡(luò)出現(xiàn)故障, purchase()方法一直在重復(fù)調(diào)用,與此同時(shí)用戶又添加了新的商品,這時(shí)網(wǎng)絡(luò)又恢復(fù)了。那么 purchase()方法獲取到 購物車數(shù)組就是錯(cuò)誤的。
為了避免這種問題,我們需要在每次新增商品時(shí),克隆 購物車數(shù)組并返回新的數(shù)組。
- //bad code
- const addItemToCart = (cart, item) => {
- cart.push({ item, date: Date.now() });
- };
- //better code
- const addItemToCart = (cart, item) => {
- return [...cart, {item, date: Date.now()}]
- };
5、全局方法
在 JavaScript 中,永遠(yuǎn)不要污染全局,會(huì)在生產(chǎn)環(huán)境中產(chǎn)生難以預(yù)料的 bug。舉個(gè)例子,比如你在 Array.prototype上新增一個(gè) diff方法來判斷兩個(gè)數(shù)組的不同。而你同事也打算做類似的事情,不過他的 diff方法是用來判斷兩個(gè)數(shù)組首位元素的不同。很明顯你們方法會(huì)產(chǎn)生沖突,遇到這類問題我們可以用 ES2015/ES6 的語法來對 Array進(jìn)行擴(kuò)展。
- //bad code
- Array.prototype.diff = function diff(comparisonArray) {
- const hash = new Set(comparisonArray);
- return this.filter(elem => !hash.has(elem));
- };
- //better code
- class SuperArray extends Array {
- diff(comparisonArray) {
- const hash = new Set(comparisonArray);
- return this.filter(elem => !hash.has(elem));
- }
- }
6、避免類型檢查
JavaScript 是無類型的,意味著你可以傳任意類型參數(shù),這種自由度很容易讓人困擾,不自覺的就會(huì)去檢查類型。仔細(xì)想想是你真的需要檢查類型還是你的 API 設(shè)計(jì)有問題?
- //bad code
- 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'));
- }
- }
- //better code
- function travelToTexas(vehicle) {
- vehicle.move(this.currentLocation, new Location('texas'));
- }
如果你需要做靜態(tài)類型檢查,比如字符串、整數(shù)等,推薦使用 TypeScript,不然你的代碼會(huì)變得又臭又長。
- //bad code
- 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');
- }
- //better code
- function combine(val1, val2) {
- return val1 + val2;
- }
復(fù)雜條件判斷
我們編寫js代碼時(shí)經(jīng)常遇到復(fù)雜邏輯判斷的情況,通常大家可以用if/else或者switch來實(shí)現(xiàn)多個(gè)條件判斷,但這樣會(huì)有個(gè)問題,隨著邏輯復(fù)雜度的增加,代碼中的if/else/switch會(huì)變得越來越臃腫,越來越看不懂,那么如何更優(yōu)雅的寫判斷邏輯
1、if/else
點(diǎn)擊列表按鈕事件
- /**
- * 按鈕點(diǎn)擊事件
- * @param {number} status 活動(dòng)狀態(tài):1 開團(tuán)進(jìn)行中 2 開團(tuán)失敗 3 商品售罄 4 開團(tuán)成功 5 系統(tǒng)取消
- */
- const onButtonClick = (status)=>{
- if(status == 1){
- sendLog('processing')
- jumpTo('IndexPage')
- }else if(status == 2){
- sendLog('fail')
- jumpTo('FailPage')
- }else if(status == 3){
- sendLog('fail')
- jumpTo('FailPage')
- }else if(status == 4){
- sendLog('success')
- jumpTo('SuccessPage')
- }else if(status == 5){
- sendLog('cancel')
- jumpTo('CancelPage')
- }else {
- sendLog('other')
- jumpTo('Index')
- }
- }
從上面我們可以看到的是通過不同的狀態(tài)來做不同的事情,代碼看起來非常不好看,大家可以很輕易的提出這段代碼的改寫方案,switch出場:
2、switch/case
- /**
- * 按鈕點(diǎn)擊事件
- * @param {number} status 活動(dòng)狀態(tài):1 開團(tuán)進(jìn)行中 2 開團(tuán)失敗 3 商品售罄 4 開團(tuán)成功 5 系統(tǒng)取消
- */
- const onButtonClick = (status)=>{
- switch (status){
- case 1:
- sendLog('processing')
- jumpTo('IndexPage')
- break
- case 2:
- case 3:
- sendLog('fail')
- jumpTo('FailPage')
- break
- case 4:
- sendLog('success')
- jumpTo('SuccessPage')
- break
- case 5:
- sendLog('cancel')
- jumpTo('CancelPage')
- break
- default:
- sendLog('other')
- jumpTo('Index')
- break
- }
- }
這樣看起來比if/else清晰多了,細(xì)心的同學(xué)也發(fā)現(xiàn)了小技巧,case 2和case 3邏輯一樣的時(shí)候,可以省去執(zhí)行語句和break,則case 2的情況自動(dòng)執(zhí)行case 3的邏輯。
3、存放到Object
將判斷條件作為對象的屬性名,將處理邏輯作為對象的屬性值,在按鈕點(diǎn)擊的時(shí)候,通過對象屬性查找的方式來進(jìn)行邏輯判斷,這種寫法特別適合一元條件判斷的情況。
- const actions = {
- '1': ['processing','IndexPage'],
- '2': ['fail','FailPage'],
- '3': ['fail','FailPage'],
- '4': ['success','SuccessPage'],
- '5': ['cancel','CancelPage'],
- 'default': ['other','Index'],
- }
- /**
- * 按鈕點(diǎn)擊事件
- * @param {number} status 活動(dòng)狀態(tài):1開團(tuán)進(jìn)行中 2開團(tuán)失敗 3 商品售罄 4 開團(tuán)成功 5 系統(tǒng)取消
- */
- const onButtonClick = (status)=>{
- let action = actions[status] || actions['default'],
- logName = action[0],
- pageName = action[1]
- sendLog(logName)
- jumpTo(pageName)
- }
4、存放到Map
- const actions = new Map([
- [1, ['processing','IndexPage']],
- [2, ['fail','FailPage']],
- [3, ['fail','FailPage']],
- [4, ['success','SuccessPage']],
- [5, ['cancel','CancelPage']],
- ['default', ['other','Index']]
- ])
- /**
- * 按鈕點(diǎn)擊事件
- * @param {number} status 活動(dòng)狀態(tài):1 開團(tuán)進(jìn)行中 2 開團(tuán)失敗 3 商品售罄 4 開團(tuán)成功 5 系統(tǒng)取消
- */
- const onButtonClick = (status)=>{
- let action = actions.get(status) || actions.get('default')
- sendLog(action[0])
- jumpTo(action[1])
- }
這樣寫用到了es6里的Map對象,是不是更爽了?Map對象和Object對象有什么區(qū)別呢?
- 一個(gè)對象通常都有自己的原型,所以一個(gè)對象總有一個(gè)"prototype"鍵。
- 一個(gè)對象的鍵只能是字符串或者Symbols,但一個(gè)Map的鍵可以是任意值。
你可以通過size屬性很容易地得到一個(gè)Map的鍵值對個(gè)數(shù),而對象的鍵值對個(gè)數(shù)只能手動(dòng)確認(rèn)。
代碼風(fēng)格
常量大寫
- //bad code
- const DAYS_IN_WEEK = 7;
- const daysInMonth = 30;
- const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
- const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
- function eraseDatabase() {}
- function restore_database() {}
- class animal {}
- class Alpaca {}
- //better code
- const DAYS_IN_WEEK = 7;
- const DAYS_IN_MONTH = 30;
- const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
- const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
- function eraseDatabase() {}
- function restoreDatabase() {}
- class Animal {}
- class Alpaca {}
先聲明后調(diào)用
- //bad code
- class PerformanceReview {
- constructor(employee) {
- this.employee = employee;
- }
- lookupPeers() {
- return db.lookup(this.employee, 'peers');
- }
- lookupManager() {
- return db.lookup(this.employee, 'manager');
- }
- getPeerReviews() {
- const peers = this.lookupPeers();
- // ...
- }
- perfReview() {
- this.getPeerReviews();
- this.getManagerReview();
- this.getSelfReview();
- }
- getManagerReview() {
- const manager = this.lookupManager();
- }
- getSelfReview() {
- // ...
- }
- }
- const review = new PerformanceReview(employee);
- review.perfReview();
- //better code
- class PerformanceReview {
- constructor(employee) {
- this.employee = employee;
- }
- perfReview() {
- this.getPeerReviews();
- this.getManagerReview();
- this.getSelfReview();
- }
- getPeerReviews() {
- const peers = this.lookupPeers();
- // ...
- }
- lookupPeers() {
- return db.lookup(this.employee, 'peers');
- }
- getManagerReview() {
- const manager = this.lookupManager();
- }
- lookupManager() {
- return db.lookup(this.employee, 'manager');
- }
- getSelfReview() {
- // ...
- }
- }
- const review = new PerformanceReview(employee);
- review.perfReview();