編寫【可讀】代碼的實踐

此為摘自 ==淘寶前端團隊(FFD)==


編寫可讀的代碼:

對于以代碼為生的程序員而已珍德,編寫可讀的代碼摔竿,是一件極其重要的事情真慢。從某種角度來說,代碼最重要的功能是能夠被閱讀榕吼,其次才是能被正確執(zhí)行饿序。一段無法正確執(zhí)行的代碼,也許會使項目延誤幾天羹蚣,但造成的危害只是暫時和短暫的原探,而一段缺乏條理,難以閱讀的代碼顽素,造成的危害卻是深遠而長久的咽弦。這里總結了下,在工作和業(yè)余生活中胁出,我對如何編寫可讀代碼這個問題的具體體會型型。


  • 變量命名

變量命名是編寫可讀代碼的基礎。只有變量被賦予一個合適的名字全蝶,才能表達它在當前環(huán)境中的意義闹蒜。

命名必須傳遞足夠的信息,形如getData這個的函數(shù)命名就沒能夠提供足夠的信息裸诽,這讓讀者無法猜測這個函數(shù)會做出些什么事情嫂用。而fetchUserInfoAsync 也許就好很多型凳,讀者至少能猜到丈冬,這個函數(shù)大約是遠程獲取用戶信息。

  • 命名的基礎

通常甘畅,我們會使用 名詞 來命名對象埂蕊,用 動詞 來命名函數(shù)往弓。比如:

        monkey.eat(banana)  // the monkey eats a banana 
    
const apple = pick(tree) // pick an apple from the tree

有時候,我們需要表示某種集合的概念蓄氧,比如數(shù)組或者哈希對象函似。這是可以使用名詞的復數(shù)形式來表示,比如bananas 表示一個數(shù)組喉童。如果需要特別強調(diào)這種集合的形式撇寞,也可以加上 ListMap 后綴來表示,比如 bananaList表示數(shù)組堂氯。 如果有些單詞的復數(shù)形式和單數(shù)形式相同蔑担,比如data , information 等,這時就用 List 為后綴表示集合概念咽白。

  • 命名的上下文

變量都是處在 上下文 (作用域)內(nèi)的啤握,變量的命名應該和上下文相契合,同一個變量晶框,在不同的上下文排抬,命名可以不同。

  • 嚴格遵循一種命名規(guī)范的收益

如果你能夠時刻按照某種嚴格的規(guī)范來命名變量和函數(shù)授段,還能帶來一個潛在的好處蹲蒲,那就是你再也不用 記住 那些之前命名過或者其他人命名過的變量和函數(shù)了。比如畴蒲,【獲取用戶信息】這個概念悠鞍,就叫作fetchUserInfomation ,不管是在早晨還是晚上模燥,家里還是在公司里咖祭,它都是命名為 fetchUserInfomation

  • 分支結構

分支是代碼里最常見的結構蔫骂,一段結構清晰的代碼單元么翰,應當是像二叉樹一樣,呈現(xiàn)下面的結構辽旋。

    if(condition1){
        if(condition2){
            ...        
        } else {
           ...
        }
    }else {
        if(condition3){
          ...
        }else {
          ...
        }
    }

這種優(yōu)美的結構能幫助我們在大腦中迅速繪制一張圖浩嫌,便于在腦海中模擬代碼的執(zhí)行。但是补胚,我們大多數(shù)人都不會遵循上面這樣的結構來寫分支代碼码耐。以下是一些常見的,可讀性比較差的分支語句的寫法:

不好的做法:在分支中return

function foo(){
    if (condition){
        // 分支1的邏輯
        return
    }
    // 分支2的邏輯
}

這種代碼很常見溶其,而且往往分支2的邏輯是先寫的骚腥,也是函數(shù)的主要邏輯。這種致命的問題就是瓶逃,如果讀者沒有注意到分支1中的 return 束铭,就不會意識到后面一段代碼(分支2)是有可能不被執(zhí)行的廓块。建議是把分支2的代碼寫著else 模塊中,代碼就會清晰可讀:

    function foo(){
        if(condition){
            // 分支1的邏輯
        }else {
            // 分支2的邏輯
        }
    }

如果某個分支為空契沫,最好留一個空行带猴,明確地告訴代碼的讀者,
如果走到這個 else 懈万,什么也不做拴清。這樣讀者就不會產(chǎn)生任何懷疑。

不好的做法:多個條件的復合

    if(condition1 && condition2 && condition3 ){
        // 分支1:做些事情
    } else {
        //分支2:做些事情
    }

這種代碼也很常見:在若干條件同時滿足(或有任一滿足) 的時候做一些主要事情(分支1)会通,否則做一些次要事情(分支2)贷掖。這樣籠統(tǒng)的使用同一段代碼來處理多個分支,那么就增加了閱讀者閱讀分支2時的負擔(需要考慮多個情況)渴语。對于這種場景苹威,通常這樣寫:

    if(condition1){
        if(condition2){
            // 分支1: 做一些事情
        }else {
            // 分支2:做一些事情
        }
    } else {
        //分支3 :做一些事情
    }

即使分支2和分支3完全一樣,我也認為有必要分開驾凶。不過對于那種多個復合條件聯(lián)系十分緊密牙甫,就沒必要分開寫,比如if(foo && foo.bar) 调违。

不好的做法:使用分支改變環(huán)境

    let foo = someValue;
    if (condition){
        foo = doSomethingTofoo(foo)
    }
    // 繼續(xù)使用 foo 做一些事情

這種風格的代碼很容易出現(xiàn)在那些屢經(jīng)修改的代碼文件中窟哺,很可能一開始是沒有這個 if 代碼塊的,后來發(fā)現(xiàn)了一個bug,于是加上了這個if 代碼塊技肩。

事實上且轨,這樣的「補丁」積累起來,很快就會摧毀代碼的可讀性和可維護性虚婿。怎么說呢旋奢?當我們在寫下上面這段代碼中的 if 分支以試圖修復 bug 的時候,我們內(nèi)心存在這樣一個假設:我們是知道程序在執(zhí)行到這一行時然痊,foo 什么樣子的至朗;但事實是,我們根本不知道剧浸,因為在這一行之前锹引,foo 很可能已經(jīng)被另一個人所寫的嘗試修復另一個 bug 的另一個 if 分支所篡改了。所以唆香,當代碼出現(xiàn)問題的時候嫌变,我們應當完整地審視一段獨立的功能代碼(通常是一個函數(shù)),并且多花一點時間來修復他躬它,比如:

    const foo = condition ? doSomethingToFoo(someValue) : someValue;

我們看到腾啥,很多風險都是在項目快速迭代的過程中積累下來的。為了「快速」迭代,在添加功能代碼的時候碑宴,我們有時候連函數(shù)這個最小單元的都不去了解,僅僅著眼于自己插入的那幾行桑谍,希望在那幾行中解決/hack掉所有問題延柠,這是十分不可取的。

我認為锣披,項目的迭代再快贞间,其代碼質(zhì)量和可讀性都應當有一個底線。這個底線是雹仿,當我們在修改代碼的時候增热,應當完整了解當前修改的這個函數(shù)的邏輯,然后修改這個函數(shù)胧辽,以達到添加功能的目的峻仇。注意,這里的「修改一個函數(shù)」和「在函數(shù)某個位置添加幾行代碼」是不同的邑商,在「修改一個函數(shù)」的時候摄咆,為了保證函數(shù)功能獨立,邏輯清晰人断,不應該畏懼在這個函數(shù)的任意位置增刪代碼吭从。


  • 函數(shù)

一個函數(shù)只做一件事情

有時,我們會自作聰明的寫一些很【通用】 的函數(shù)恶迈。比如涩金,我們有可能寫出下面這樣一個獲取用戶信息的函數(shù) fetchUserInfo:其邏輯是:

1、當傳入的參數(shù)是用戶ID(字符串)時暇仲,返回單個用戶的數(shù)據(jù)步做。
2、當傳入的參數(shù)是用戶ID的列表(數(shù)組)時奈附,返回一個數(shù)組辆床,其中每項包含一個用戶的數(shù)據(jù)

    sync function fetchUserInfo (id){
        const isSingle = typeof id ==="string";
        const idList = isSingle ? [id] : id;
        const result = await request.post('/api/userInfo',{idList});
        return isSingle ? result[0] :result;
    }
    
    //  可以這樣調(diào)用
    const userList = await fetchUserInfo(['1001','1013']);
    
    // 也可以這樣調(diào)用
    const user = await fetchUserInfo('1013')

這個函數(shù)能做兩件事:1)獲取多個用戶列表的數(shù)據(jù);2)獲取單個用戶數(shù)據(jù)桅狠。這樣讀者在某處讀到 fetchUserInfo(['1001','1013']) 這句調(diào)用代碼時讼载,會立刻對 fetchUserInfo 產(chǎn)生第一印象:這個函數(shù)是需要出入用戶ID數(shù)組的;而當讀到另一種調(diào)用方式時中跌,就會懷疑自己之前的判斷咨堤。

遵循一個函數(shù)只做一件事 的原則,我們可以將上述功能拆分成兩個函數(shù) fetchMultipleUserfetchSingleUser 來實現(xiàn)漩符。

    async function fetchMultipleUser(idList){
        return await request.post('/api/users/',{idList})
    }
    
    async function fetchSingleUser(id){
        return await fetchMultipleUser([id])[0]
    }

改良后的代碼不僅改善了代碼的可讀性一喘,也改善了可維護性,當不需要獲取單一用戶信息時,就可以放心大膽的直接刪掉整個函數(shù)凸克。

如何界定某個函數(shù)做的是不是一件事情议蟆?
作者的經(jīng)驗是:如果一個函數(shù)的參數(shù)僅僅包含輸入數(shù)據(jù)(交給函數(shù)處理的數(shù)據(jù)),而沒有混雜或暗含有指令(以某種約定的方式告訴函數(shù)該怎么處理數(shù)據(jù))萎战,那么函數(shù)做的應當就是一件事情咐容。比如說,改良前的 fetchUserInfo函數(shù)的參數(shù)是【多個用戶ID數(shù)組或單個用戶的ID】蚂维,這個【或】字其實就暗含了某種指令戳粒。

函數(shù)應適當?shù)靥幚懋惓?/strong>

有時候,我們會陷入一種很不好的習慣中虫啥,那就是蔚约,總是去嘗試寫出永遠不會報錯的函數(shù)。我們會給參數(shù)配上默認值涂籽,在很多地方使用 || 或者 && 來避免代碼運行出錯苹祟,仿佛如果你的函數(shù)報錯會成為某種恥辱似的。而且评雌,當我們嘗試去修復一個運行時報錯的函數(shù)時苔咪,我們往往傾向于在報錯的那一行添加一些兼容邏輯來避免報錯。

舉個例子柳骄,假設我們需要編寫一個獲取用戶詳情的函數(shù)团赏,它要返回一個完整的用戶信息對象:不僅包含ID,名字等基本信息耐薯,也包含諸如「收藏的書籍」等通過額外接口返回的信息舔清。這些額外的接口也許不太穩(wěn)定:

    async function getUserDetail(id) {
  const user = await fetchSingleUser(id);
  user.favoriteBooks = (await fetchUserFavorits(id)).books;
  // 上面這一行報錯了:Can not read property 'books' of undefined.
  // ...
}

假設 fetchUserFavorites 會時不時地返回 undefined,那么讀取其 books 屬性自然就會報錯曲初。為了修復該問題体谒,我們很可能會這樣做:

    const favorites = await fetchUserFavorits(id);
user.favoriteBooks = favorites && favorites.books;
// 這下不會報錯了

這樣做看似解決了問題:的確,getUserDetail 不會再報錯了臼婆,但同時埋下了更深的隱患抒痒。

當 fetchUserFavorites 返回 undefined 時,程序已經(jīng)處于一種異常狀態(tài)了颁褂,我們沒有任何理由放任程序繼續(xù)運行下去故响。試想,如果后面的某個時刻(比如用戶點擊「我收藏的書」選項卡)颁独,程序試圖遍歷 user.favoriteBooks 屬性(它被賦值成了undefined)彩届,那時也會報錯,而且那時排查起來會更加困難誓酒。

如何處理上述的情況呢樟蠕?我認為,如果被我們依賴的 fetchUserFavorits 屬于當前的項目,那么 getUserDetail 對此報錯真的沒什么責任寨辩,因為 fetchUserFavorits 就不應該返回 undefined吓懈,我們應該去修復 fetchUserFavorits,任務失敗時顯式地告知出來靡狞,或者直接拋出異常耻警。同時,getUserDetail 稍作修改:

    // 情況1:顯式告知耍攘,此時應認為獲取不到收藏數(shù)據(jù)不算致命的錯誤
const result = await fetchUserFavorits(id);
if(result.success) {
  user.favoriteBooks = result.data.books;
} else {
  user.favoriteBooks = []
}

// 情況2:直接拋出異常
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 這時 `getUserDetail` 不需要改動,任由異常沿著調(diào)用棧向上冒泡

那么如果 fetchUserFavorits 不在當前項目中畔勤,而是依賴的外部模塊呢蕾各?我認為,這時你就該為選擇了這樣一個不可靠的模塊負責庆揪,在 getUserDetail 中增加一些「擦屁股」代碼式曲,來避免你的項目的其他部分受到侵害。

    const favorites = await fetchUserFavorits(id);
if(favorites) {
  user.favoriteBooks = favorites.books;
} else {
  throw new Error('獲取用戶收藏失敗');
}

控制函數(shù)的副作用

無副作用的函數(shù)缸榛,是不依賴上下文吝羞,也不改變上下文的函數(shù)。長久依賴内颗,我們已經(jīng)習慣了去寫「有副作用的函數(shù)」钧排,畢竟 JavaScript 需要通過副作用去操作環(huán)境的 API 完成任務。這就導致了均澳,很多原本可以用純粹的恨溜、無副作用的函數(shù)完成任務的場合,我們也會不自覺地采取有副作用的方式找前。

雖然看上去有點可笑糟袁,但我們有時候就是會寫出下面這樣的代碼!

async function getUserDetail(id) {
  const user = await fetchSingleUserInfo(id);
  await addFavoritesToUser(user);
  ...
}
async function addFavoritesToUser(user) {
  const result = await fetchUserFavorits(user.id);
  user.favoriteBooks = result.books;
  user.favoriteSongs = result.songs;
  user.isMusicFan = result.songs.length > 100;
}

上面躺盛,addFavoritesToUser 函數(shù)就是一個「有副作用」的函數(shù)项戴,它改變了 users,給它新增了幾個個字段槽惫。問題在于周叮,僅僅閱讀 getUserData 函數(shù)的代碼完全無法知道,user 會發(fā)生怎樣的改變界斜。

一個無副作用的函數(shù)應該是這樣的:

    async function getUserDetail(id) {
  const user = await fetchSingleUserInfo(id);
  const {books, songs, isMusicFan} = await getUserFavorites(id);
  return Object.assign(user, {books, songs, isMusicFan})
}
async function getUserFavorites(id) {
  const {books, songs} = await fetchUserFavorits(user.id);
  return {
    books, songs, isMusicFan: result.songs.length > 100
  }
}

非侵入性地改造函數(shù)

函數(shù)是一段獨立和內(nèi)聚的邏輯则吟。在產(chǎn)品迭代的過程中,我們有時候不得不去修改函數(shù)的邏輯锄蹂,為其添加一些新特性氓仲。之前我們也說過,一個函數(shù)只應做一件事,如果我們需要添加的新特性敬扛,與原先函數(shù)中的邏輯沒有什么聯(lián)系晰洒,那么決定是否通過改造這個函數(shù)來添加新功能,應當格外謹慎啥箭。

仍然用「向服務器查詢用戶數(shù)據(jù)」為例谍珊,假設我們有如下這樣一個函數(shù)(為了讓它看上去復雜一些,假設我們使用了一個更基本的 request 庫):

    const fetchUserInfo = (userId, callback) => {
  const param = {
    url: '/api/user',
    method: 'post',
    payload: {id: userId}
  };
  request(param, callback);
}

現(xiàn)在有了一個新需求:為 fetchUserInfo 函數(shù)增加一道本地緩存急侥,如果第二次請求同一個 userId 的用戶信息砌滞,就不再重新向服務器發(fā)起請求,而直接以第一次請求得到的數(shù)據(jù)返回坏怪。

按照如下快捷簡單的解決方案贝润,改造這個函數(shù)只需要五分鐘時間:

    const userInfoMap = {};
const fetchUserInfo = (userId, callback) => {
  if (userInfoMap[userId]) {            // 新增代碼
    callback(userInfoMap[userId]);    // 新增代碼
  } else {                              // 新增代碼
    const param = {
      // ... 參數(shù)
    };
    request(param, (result) => {
      userInfoMap[userId] = result;   // 新增代碼
      callback(result);
    });
  }
}

不知你有沒有發(fā)現(xiàn),經(jīng)此改造铝宵,這個函數(shù)的可讀性已經(jīng)明顯降低了打掘。沒有緩存機制前,函數(shù)很清晰鹏秋,一眼就能明白尊蚁,加上新增的幾行代碼,已經(jīng)不能一眼就看明白了侣夷。

實際上横朋,「緩存」和「獲取用戶數(shù)據(jù)」完全是獨立的兩件事。我提出的方案是百拓,編寫一個通用的緩存包裝函數(shù)(類似裝飾器)memorizeThunk叶撒,對 fetchUserInfo 進行包裝,產(chǎn)出一個新的具有緩存功能的 fetchUserInfoCache耐版,在不破壞原有函數(shù)可讀性的基礎上祠够,提供緩存功能。

    const memorizeThunk = (func, reducer) => {
  const cache = {};
  return (...args, callback) => {
    const key = reducer(...args);
    if (cache[key]) {
      callback(...cache[key]);
    } else {
      func(...args, (...result) => {
        cache[key] = result;
        callback(...result);
      });
    }
  }
}
const fetchUserInfo = (userInfo, callback) => {
  // 原來的邏輯
}
const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);

也許實現(xiàn)這個方案需要十五分鐘粪牲,但是試想一下古瓤,如果將來的某個時候,我們又不需要緩存功能了(或者需要提供一個開關來打開/關閉緩存功能)腺阳,修改代碼的負擔是怎樣的落君?第一種簡單方案,我們需要精準(提心吊膽地)地刪掉新增的若干行代碼亭引,而我提出的這種方案绎速,是以函數(shù)為單位增刪的,負擔要輕很多焙蚓,不是嗎纹冤?

類的結構

避免濫用成員函數(shù)

總結

偉大的文學作品都是建立在廢紙堆上的洒宝,不斷刪改作品的過程有助于寫作者培養(yǎng)良好的「語感」。當然萌京,代碼畢竟不是藝術品雁歌,程序員沒有精力也不一定有必要像作家一樣反復打磨自己的代碼/作品。但是知残,如果我們能夠在編寫代碼時稍稍多考慮一下實現(xiàn)的合理性靠瞎,或者在添加新功能的時候稍稍回顧一下之前的實現(xiàn),我們就能夠培養(yǎng)出一些「代碼語感」求妹。這種「代碼語感」會非常有助于我們寫出高質(zhì)量的可讀的代碼乏盐。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市制恍,隨后出現(xiàn)的幾起案子父能,更是在濱河造成了極大的恐慌,老刑警劉巖吧趣,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件法竞,死亡現(xiàn)場離奇詭異耙厚,居然都是意外死亡强挫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門薛躬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俯渤,“玉大人,你說我怎么就攤上這事型宝“私常” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵趴酣,是天一觀的道長梨树。 經(jīng)常有香客問我,道長岖寞,這世上最難降的妖魔是什么抡四? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮仗谆,結果婚禮上指巡,老公的妹妹穿的比我還像新娘。我一直安慰自己隶垮,他們只是感情好藻雪,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狸吞,像睡著了一般勉耀。 火紅的嫁衣襯著肌膚如雪指煎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天瑰排,我揣著相機與錄音贯要,去河邊找鬼。 笑死椭住,一個胖子當著我的面吹牛崇渗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播京郑,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宅广,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了些举?” 一聲冷哼從身側(cè)響起跟狱,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎户魏,沒想到半個月后驶臊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡叼丑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年关翎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸠信。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡纵寝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出星立,到底是詐尸還是另有隱情爽茴,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布绰垂,位于F島的核電站室奏,受9級特大地震影響秽之,放射性物質(zhì)發(fā)生泄漏泽本。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一暖璧、第九天 我趴在偏房一處隱蔽的房頂上張望酱畅。 院中可真熱鬧琳袄,春花似錦、人聲如沸纺酸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽餐蔬。三九已至碎紊,卻和暖如春佑附,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仗考。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工音同, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人秃嗜。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓权均,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锅锨。 傳聞我的和親對象是個殘疾皇子叽赊,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 本來想寫一個怎么寫好代碼的主題,昨天突然看到了淘寶團隊的這篇文章《編寫「可讀」代碼的實踐》必搞,把我想到的沒想到的基本...
    唯泥Bernie閱讀 1,211評論 0 4
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,114評論 25 707
  • 一必指、 人的一生中,每個人都避免不了的會有焦慮情緒出現(xiàn)恕洲,區(qū)別僅在于焦慮的時間與程度不同塔橡。列子不勝枚舉,最常見的是各類...
    萱源mama閱讀 1,648評論 2 5
  • 在人來人往的街頭霜第,獨來獨往 在燈火繁鬧的街尾葛家,摸黑獨行
    Yaweix閱讀 194評論 0 0
  • 今天“分答”免費聽,捧著手機聽職業(yè)生涯規(guī)劃師趙昂老師對于職場的各種解答庶诡,盡管沒有特別契合自己現(xiàn)狀的回答惦银,但是還...
    簡約Dr閱讀 1,601評論 5 5