我們或多或少都聽過“數據綁定”這個詞,“數據綁定”的關鍵在于監聽數據的變化,可是對于這樣一個對象:var obj={value: 1},我們該怎么知道 obj 發生了改變呢?
definePropety
ES5 提供了 Object.defineProperty 方法,該方法可以在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,并返回這個對象。
語法
Object.defineProperty(obj, prop, descriptor)
參數
obj: 要在其上定義屬性的對象。 prop: 要定義或修改的屬性的名稱。 descriptor: 將被定義或修改的屬性的描述符。
舉個例子:
var obj={}; Object.defineProperty(obj, "num", { value : 1, writable : true, enumerable : true, configurable : true }); // 對象 obj 擁有屬性 num,值為 1
雖然我們可以直接添加屬性和值,但是使用這種方式,我們能進行更多的配置。
函數的第三個參數 descriptor 所表示的屬性描述符有兩種形式:數據描述符和存取描述符。
兩者均具有以下兩種鍵值:
configurable
當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,也能夠被刪除。默認為 false。
enumerable
當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在對象的枚舉屬性中。默認為 false。
數據描述符同時具有以下可選鍵值:
value
該屬性對應的值??梢允侨魏斡行У?JavaScript 值(數值,對象,函數等)。默認為 undefined。
writable
當且僅當該屬性的 writable 為 true 時,該屬性才能被賦值運算符改變。默認為 false。
存取描述符同時具有以下可選鍵值:
get
一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認為 undefined。
set
一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一參數,并將該參數的新值分配給該屬性。默認為 undefined。
值得注意的是:
屬性描述符必須是數據描述符或者存取描述符兩種形式之一,不能同時是兩者。這就意味著你可以:
Object.defineProperty({}, "num", { value: 1, writable: true, enumerable: true, configurable: true });
也可以:
var value=1; Object.defineProperty({}, "num", { get : function(){ return value; }, set : function(newValue){ value=newValue; }, enumerable : true, configurable : true });
但是不可以:
// 報錯 Object.defineProperty({}, "num", { value: 1, get: function() { return 1; } });
此外,所有的屬性描述符都是非必須的,但是 descriptor 這個字段是必須的,如果不進行任何配置,你可以這樣:
var obj=Object.defineProperty({}, "num", {}); console.log(obj.num); // undefined
Setters 和 Getters
之所以講到 defineProperty,是因為我們要使用存取描述符中的 get 和 set,這兩個方法又被稱為 getter 和 setter。由 getter 和 setter 定義的屬性稱做”存取器屬性“。
當程序查詢存取器屬性的值時,JavaScript 調用 getter方法。這個方法的返回值就是屬性存取表達式的值。當程序設置一個存取器屬性的值時,JavaScript 調用 setter 方法,將賦值表達式右側的值當做參數傳入 setter。從某種意義上講,這個方法負責“設置”屬性值。可以忽略 setter 方法的返回值。
舉個例子:
var obj={}, value=null; Object.defineProperty(obj, "num", { get: function(){ console.log('執行了 get 操作') return value; }, set: function(newValue) { console.log('執行了 set 操作') value=newValue; } }) obj.value=1 // 執行了 set 操作 console.log(obj.value); // 執行了 get 操作 // 1
這不就是我們要的監控數據改變的方法嗎?我們再來封裝一下:
function Archiver() { var value=null; // archive n. 檔案 var archive=[]; Object.defineProperty(this, 'num', { get: function() { console.log('執行了 get 操作') return value; }, set: function(value) { console.log('執行了 set 操作') value=value; archive.push({ val: value }); } }); this.getArchive=function() { return archive; }; } var arc=new Archiver(); arc.num; // 執行了 get 操作 arc.num=11; // 執行了 set 操作 arc.num=13; // 執行了 set 操作 console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]
watch API
既然可以監控數據的改變,那我可以這樣設想,即當數據改變的時候,自動進行渲染工作。舉個例子:
HTML 中有個 span 標簽和 button 標簽
<span id="container">1</span> <button id="button">點擊加 1</button>
當點擊按鈕的時候,span 標簽里的值加 1。
傳統的做法是:
document.getElementById('button').addEventListener("click", function(){ var container=document.getElementById("container"); container.innerHTML=Number(container.innerHTML) + 1; });
如果使用了 defineProperty:
var obj={ value: 1 } // 儲存 obj.value 的值 var value=1; Object.defineProperty(obj, "value", { get: function() { return value; }, set: function(newValue) { value=newValue; document.getElementById('container').innerHTML=newValue; } }); document.getElementById('button').addEventListener("click", function() { obj.value +=1; });
代碼看似增多了,但是當我們需要改變 span 標簽里的值的時候,直接修改 obj.value 的值就可以了。
然而,現在的寫法,我們還需要單獨聲明一個變量存儲 obj.value 的值,因為如果你在 set 中直接 obj.value=newValue 就會陷入無限的循環中。此外,我們可能需要監控很多屬性值的改變,要是一個一個寫,也很累吶,所以我們簡單寫個 watch 函數。使用效果如下:
var obj={ value: 1 } watch(obj, "num", function(newvalue){ document.getElementById('container').innerHTML=newvalue; }) document.getElementById('button').addEventListener("click", function(){ obj.value +=1 });
我們來寫下這個 watch 函數:
(function(){ var root=this; function watch(obj, name, func){ var value=obj[name]; Object.defineProperty(obj, name, { get: function() { return value; }, set: function(newValue) { value=newValue; func(value) } }); if (value) obj[name]=value } this.watch=watch; })()
現在我們已經可以監控對象屬性值的改變,并且可以根據屬性值的改變,添加回調函數,棒棒噠~
proxy
使用 defineProperty 只能重定義屬性的讀?。╣et)和設置(set)行為,到了 ES6,提供了 Proxy,可以重定義更多的行為,比如 in、delete、函數調用等更多行為。
Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例。我們來看看它的語法:
var proxy=new Proxy(target, handler);
proxy 對象的所有用法,都是上面這種形式,不同的只是handler參數的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定制攔截行為。
var proxy=new Proxy({}, { get: function(obj, prop) { console.log('設置 get 操作') return obj[prop]; }, set: function(obj, prop, value) { console.log('設置 set 操作') obj[prop]=value; } }); proxy.time=35; // 設置 set 操作 console.log(proxy.time); // 設置 get 操作 // 35
除了 get 和 set 之外,proxy 可以攔截多達 13 種操作,比如 has(target, propKey),可以攔截 propKey in proxy 的操作,返回一個布爾值。
// 使用 has 方法隱藏某些屬性,不被 in 運算符發現 var handler={ has (target, key) { if (key[0]==='_') { return false; } return key in target; } }; var target={ _prop: 'foo', prop: 'foo' }; var proxy=new Proxy(target, handler); console.log('_prop' in proxy); // false
又比如說 apply 方法攔截函數的調用、call 和 apply 操作。
apply 方法可以接受三個參數,分別是目標對象、目標對象的上下文對象(this)和目標對象的參數數組,不過這里我們簡單演示一下:
var target=function () { return 'I am the target'; }; var handler={ apply: function () { return 'I am the proxy'; } }; var p=new Proxy(target, handler); p(); // "I am the proxy"
又比如說 ownKeys 方法可以攔截對象自身屬性的讀取操作。具體來說,攔截以下操作:
下面的例子是攔截第一個字符為下劃線的屬性名,不讓它被 for of 遍歷到。
let target={ _bar: 'foo', _prop: 'bar', prop: 'baz' }; let handler={ ownKeys (target) { return Reflect.ownKeys(target).filter(key=> key[0] !=='_'); } }; let proxy=new Proxy(target, handler); for (let key of Object.keys(proxy)) { console.log(target[key]); } // "baz"
更多的攔截行為可以查看阮一峰老師的 《ECMAScript 6 入門》
值得注意的是,proxy 的最大問題在于瀏覽器支持度不夠,而且很多效果無法使用 poilyfill 來彌補。
watch API 優化
我們使用 proxy 再來寫一下 watch 函數。使用效果如下:
(function() { var root=this; function watch(target, func) { var proxy=new Proxy(target, { get: function(target, prop) { return target[prop]; }, set: function(target, prop, value) { target[prop]=value; func(prop, value); } }); if(target[name]) proxy[name]=value; return proxy; } this.watch=watch; })() var obj={ value: 1 } var newObj=watch(obj, function(key, newvalue) { if (key=='value') document.getElementById('container').innerHTML=newvalue; }) document.getElementById('button').addEventListener("click", function() { newObj.value +=1 });
我們也可以發現,使用 defineProperty 和 proxy 的區別,當使用 defineProperty,我們修改原來的 obj 對象就可以觸發攔截,而使用 proxy,就必須修改代理對象,即 Proxy 的實例才可以觸發攔截。
作者:冴羽
本文主要是講解 <script setup> 與 TypeScript 的基本使用。
<script setup> 是什么?
<script setup> 是在單文件組件 (SFC) 中使用 composition api 的編譯時語法糖。
本文寫作時,vue 使用的 3.2.26 版本。
我們先看看 vue3 <script setup> 的發展歷程:
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="add">count is: {{ count }}</button>
<ComponentA />
<ComponentB />
</template>
<script>
import { defineComponent, ref } from 'vue'
import ComponentA from '@/components/ComponentA'
import ComponentB from '@/components/ComponentB'
export default defineComponent({
name: 'HelloWorld',
components: { ComponentA, ComponentB },
props: {
msg: String,
},
setup(props, ctx) {
const count=ref(0)
function add() {
count.value++
}
// 使用return {} 把變量、方法暴露給模板
return {
count,
add,
}
},
})
</script>
<script setup lang="ts">
import { ref } from 'vue'
import ComponentA from '@/components/ComponentA'
import ComponentB from '@/components/ComponentB'
defineProps<{ msg: string }>()
const count=ref(0)
function add() {
count.value++
}
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="add">count is: {{ count }}</button>
<ComponentA />
<ComponentB />
</template>
與組件選項 setup 函數對比, <script setup> 的優點:
當然, <script setup> 也是有自己的缺點的,比如需要學習額外的東西 API。
那么 <script setup> 怎么使用呢?有哪些使用要點?與TypeScript如何結合?
Vue3 單文件組件 (SFC) 的 TS IDE 支持請用 <script setup lang="ts"> + VSCode + Volar。
類型檢查使用 vue-tsc 命令。
將 setup 屬性添加到 <script> 代碼塊上。
<script setup>
import { ref } from 'vue'
defineProps({
msg: String
})
const count=ref(0)
function add() {
count.value++
}
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="add">count is: {{ count }}</button>
</template>
若需要使用 TypeScript,則將 lang 屬性添加到 <script> 代碼塊上,并賦值 ts。
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count=ref(0)
function add() {
count.value++
}
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="add">count is: {{ count }}</button>
</template>
<script setup> 塊中的腳本會被編譯成組件選項 setup 函數的內容,也就是說它會在每次組件實例被創建的時候執行。
在 <script setup> 聲明的頂層綁定(變量、函數、import引入的內容),都會自動暴露給模板,在模板中直接使用。
<script setup>
import { ref } from 'vue'
// 外部引入的方法,不需要通過 methods 選項來暴露它,模板可以直接使用
import { getToken } from './utils'
// 外部引入的組件,不需要通過 components 選項來暴露它,模板可以直接使用
import ComponentA from '@/components/ComponentA'
defineProps({
msg: String
})
// 變量聲明,模板可以直接使用
const count=ref(0)
// 函數聲明,模板可以直接使用
function add() {
count.value++
}
</script>
<template>
<h1>{{ msg }}</h1>
<h1>{{ getToken() }}</h1>
<button type="button" @click="add">count is: {{ count }}</button>
<ComponentA />
</template>
注意:
編譯器宏(compiler macros) 有:defineProps、defineEmits、withDefaults、defineExpose 等。
編譯器宏只能在 <script setup> 塊中使用,不需要被導入,并且會在處理 <script setup> 塊時被一同編譯掉。
編譯器宏必須在 <script setup> 的頂層使用,不可以在 <script setup> 的局部變量中引用。
在 <script setup> 塊中是沒有組件配置項的,也就是說是沒有 props 選項,需要使用 defineProps 來聲明 props 相關信息。defineProps 接收的對象和組件選項 props 的值一樣。
<script setup>
const props=defineProps({
msg: String,
title: {
type: String,
default: '我是標題'
},
list: {
type: Array,
default: ()=> []
}
})
// 在 js 中使用 props 中的屬性
console.log(props.msg)
</script>
<template>
<!-- 在模板中直接使用 props 中聲明的變量 -->
<h1>{{ msg }}</h1>
<div>{{ title }}</div>
</template>
TS 版本:
<script setup lang="ts">
interface ListItem {
name: string
age: number
}
const props=defineProps<{
msg: string
title: string
list: ListItem[]
}>()
// 在 ts 中使用 props 中的屬性,具有很好的類型推斷能力
console.log(props.list[0].age)
</script>
<template>
<h1>{{ msg }}</h1>
<div>{{ title }}</div>
</template>
從代碼中可以發現 TS 寫法里 props 沒有定義默認值。
Vue3 為我們提供了 withDefaults 這個編譯器宏,給 props 提供默認值。
<script setup lang="ts">
interface ListItem {
name: string
age: number
}
interface Props {
msg: string
// title可選
title?: string
list: ListItem[]
}
// withDefaults 的第二個參數便是默認參數設置,會被編譯為運行時 props 的 default 選項
const props=withDefaults(defineProps<Props>(), {
title: '我是標題',
// 對于array、object需要使用函數,和以前的寫法一樣
list: ()=> []
})
// 在 ts 中使用 props 中的屬性,具有很好的類型推斷能力
console.log(props.list[0].age)
</script>
<template>
<h1>{{ msg }}</h1>
<div>{{ title }}</div>
</template>
一個需要注意的地方:在頂層聲明一個和props的屬性同名的變量,會有些問題。
<script setup>
const props=defineProps({
title: {
type: String,
default: '我是標題'
}
})
// 在頂層聲明一個和props的屬性title同名的變量
const title='123'
</script>
<template>
<!-- props.title 顯示的是 props.title 的值,‘我是標題’ -->
<div>{{ props.title }}</div>
<!-- title 顯示的是 在頂層聲明的 title 的值,‘123’ -->
<div>{{ title }}</div>
</template>
所以,和組件選項一樣,不要定義和 props 的屬性同名的頂層變量。
一樣的,在 <script setup> 塊中也是沒有組件配置項 emits 的,需要使用 defineEmits 編譯器宏聲明 emits 相關信息。
// ./components/HelloWorld.vue
<script setup>
defineProps({
msg: String,
})
const emits=defineEmits(['changeMsg'])
const handleChangeMsg=()=> {
emits('changeMsg', 'Hello TS')
}
</script>
<template>
<h1>{{ msg }}</h1>
<button @click="handleChangeMsg">handleChangeMsg</button>
</template>
使用組件:
<script setup>
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const msg=ref('Hello Vue3')
const changeMsg=(v)=> {
msg.value=v
}
</script>
<template>
<HelloWorld :msg="msg" @changeMsg="changeMsg" />
</template>
TS 版本:
// ./components/HelloWorld.vue
<script setup lang="ts">
defineProps<{
msg: string
}>()
const emits=defineEmits<{
(e: 'changeMsg', value: string): void
}>()
const handleChangeMsg=()=> {
emits('changeMsg', 'Hello TS')
}
</script>
<template>
<h1>{{ msg }}</h1>
<button @click="handleChangeMsg">handleChangeMsg</button>
</template>
使用組件:
<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const msg=ref('Hello Vue3')
const changeMsg=(v: string)=> {
msg.value=v
}
</script>
<template>
<HelloWorld :msg="msg" @changeMsg="changeMsg" />
</template>
在 Vue3 中,默認不會暴露任何在 <script setup> 中聲明的綁定,即不能通過模板 ref 獲取到組件實例聲明的綁定。
Vue3 提供了 defineExpose 編譯器宏,可以顯式地暴露需要暴露的組件中聲明的變量和方法。
// ./components/HelloWorld.vue
<script setup>
import { ref } from 'vue'
const msg=ref('Hello Vue3')
const handleChangeMsg=(v)=> {
msg.value=v
}
// 對外暴露的屬性
defineExpose({
msg,
handleChangeMsg,
})
</script>
<template>
<h1>{{ msg }}</h1>
</template>
使用組件:
<script setup>
import { ref, onMounted } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const root=ref(null)
onMounted(()=> {
console.log(root.value.msg)
})
const handleChangeMsg=()=> {
root.value.handleChangeMsg('Hello TS')
}
</script>
<template>
<HelloWorld ref="root" />
<button @click="handleChangeMsg">handleChangeMsg</button>
</template>
TS 版本:
// ./components/HelloWorld.vue
<script setup lang="ts">
import { ref } from 'vue'
const msg=ref('Hello Vue3')
const handleChangeMsg=(v: string)=> {
msg.value=v
}
defineExpose({
msg,
handleChangeMsg
})
</script>
<template>
<h1>{{ msg }}</h1>
</template>
使用組件:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
// 此處暫時使用any,需要定義類型
const root=ref<any>(null)
onMounted(()=> {
console.log(root.value.msg)
})
const handleChangeMsg=()=> {
root.value.handleChangeMsg('Hello TS')
}
</script>
<template>
<HelloWorld ref="root" />
<button @click="handleChangeMsg">handleChangeMsg</button>
</template>
在 <script setup> 中常用的輔助函數hooks api,主要有:useAttrs、useSlots、useCssModule,其他的輔助函數還在實驗階段,不做介紹。
在模板中使用 $attrs 來訪問 attrs 數據,與 Vue2 相比,Vue3 的 $attrs 還包含了 class 和 style 屬性。
在 <script setup> 中使用 useAttrs 函數獲取 attrs 數據。
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld class="hello-word" title="我是標題" />
</template>
// ./components/HelloWorld.vue
<script setup>
import { useAttrs } from 'vue'
const attrs=useAttrs()
// js中使用
console.log(attrs.class) // hello-word
console.log(attrs.title) // 我是標題
</script>
<template>
<!-- 在模板中使用 $attrs 訪問屬性 -->
<div>{{ $attrs.title }}</div>
</template>
在模板中使用 $slots 來訪問 slots 數據。
在 <script setup> 中使用 useSlots 函數獲取 slots 插槽數據。
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld>
<div>默認插槽</div>
<template v-slot:footer>
<div>具名插槽footer</div>
</template>
</HelloWorld>
</template>
<script setup>
import { useSlots } from 'vue'
const slots=useSlots()
// 在js中訪問插槽默認插槽default、具名插槽footer
console.log(slots.default)
console.log(slots.footer)
</script>
<template>
<div>
<!-- 在模板中使用插槽 -->
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
在 Vue3 中,也是支持 CSS Modules 的,在 <style> 上增加 module 屬性,即<style module> 。
<style module> 代碼塊會被編譯為 CSS Modules 并且將生成的 CSS 類作為 $style 對象的鍵暴露給組件,可以直接在模板中使用 $style。而對于如 <style module="content"> 具名 CSS Modules,編譯后生成的 CSS 類作為 content 對象的鍵暴露給組件,即module 屬性值什么,就暴露什么對象。
<script setup lang="ts">
import { useCssModule } from 'vue'
// 不傳遞參數,獲取<style module>代碼塊編譯后的css類對象
const style=useCssModule()
console.log(style.success) // 獲取到的是success類名經過 hash 計算后的類名
// 傳遞參數content,獲取<style module="content">代碼塊編譯后的css類對象
const contentStyle=useCssModule('content')
</script>
<template>
<div class="success">普通style red</div>
<div :class="$style.success">默認CssModule pink</div>
<div :class="style.success">默認CssModule pink</div>
<div :class="contentStyle.success">具名CssModule blue</div>
<div :class="content.success">具名CssModule blue</div>
</template>
<!-- 普通style -->
<style>
.success {
color: red;
}
</style>
<!-- 無值的css module -->
<style module lang="less">
.success {
color: pink;
}
</style>
<!-- 具名的css module -->
<style module="content" lang="less">
.success {
color: blue;
}
</style>
注意,同名的CSS Module,后面的會覆蓋前面的。
在組件選項中,模板需要使用組件(除了全局組件),需要在 components 選項中注冊。
而在 <script setup> 中組件不需要再注冊,模板可以直接使用,其實就是相當于一個頂層變量。
建議使用大駝峰(PascalCase)命名組件和使用組件。
<script setup>
import HelloWorld from './HelloWorld.vue'
</script>
<template>
<HelloWorld />
</template>
<script setup> 是沒有組件配置項 name 的,可以再使用一個普通的 <script> 來配置 name。
// ./components/HelloWorld.vue
<script>
export default {
name: 'HelloWorld'
}
</script>
<script setup>
import { ref } from 'vue'
const total=ref(10)
</script>
<template>
<div>{{ total }}</div>
</template>
使用:
<script setup>
import HelloWorld from './components/HelloWorld.vue'
console.log(HelloWorld.name) // 'HelloWorld'
</script>
<template>
<HelloWorld />
</template>
注意:如果你設置了 lang 屬性,<script setup> 和 <script> 的 lang 需要保持一致。
inheritAttrs 表示是否禁用屬性繼承,默認值是 true。
<script setup> 是沒有組件配置項 inheritAttrs 的,可以再使用一個普通的 <script>。
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld title="我是title"/>
</template>
./components/HelloWorld.vue
<script>
export default {
name: 'HelloWorld',
inheritAttrs: false,
}
</script>
<script setup>
import { useAttrs } from 'vue'
const attrs=useAttrs()
</script>
<template>
<div>
<span :title="attrs.title">hover一下看title</span>
<span :title="$attrs.title">hover一下看title</span>
</div>
</template>
<script setup> 中可以使用頂層 await。結果代碼會被編譯成 async setup()
<script setup>
const userInfo=await fetch(`/api/post/getUserInfo`)
</script>
注意:async setup() 必須與 Suspense 組合使用,Suspense 目前還是處于實驗階段的特性,其 API 可能隨時會發生變動,建議暫時不要使用。
在 vue3 中,我們可以使用點語法來使用掛載在一個對象上的組件。
// components/Form/index.js
import Form from './Form.vue'
import Input from './Input.vue'
import Label from './Label.vue'
// 把Input、Label組件掛載到 Form 組件上
Form.Input=Input
Form.Label=Label
export default Form
// 使用:
<script setup lang="ts">
import Form from './components/Form'
</script>
<template>
<Form>
<Form.Label />
<Form.Input />
</Form>
</template>
命名空間組件在另外一種場景中的使用,從單個文件中導入多個組件時:
// FormComponents/index.js
import Input from './Input.vue'
import Label from './Label.vue'
export default {
Input,
Label,
}
// 使用
<script setup>
import * as Form from './FormComponents'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
Vue3 中 <style> 標簽可以通過 v-bind 這一 CSS 函數將 CSS 的值關聯到動態的組件狀態上。
<script setup>
const theme={
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
// 使用頂層綁定
color: v-bind('theme.color');
}
</style>
全局指令:
<template>
<div v-click-outside />
</template>
自定義指令:
<script setup>
import { ref } from 'vue'
const total=ref(10)
// 自定義指令
// 必須以 小寫字母v開頭的小駝峰 的格式來命名本地自定義指令
// 在模板中使用時,需要用中劃線的格式表示,不可直接使用vMyDirective
const vMyDirective={
beforeMount: (el, binding, vnode)=> {
el.style.borderColor='red'
},
updated(el, binding, vnode) {
if (el.value % 2 !==0) {
el.style.borderColor='blue'
} else {
el.style.borderColor='red'
}
},
}
const add=()=> {
total.value++
}
</script>
<template>
<input :value="total" v-my-directive />
<button @click="add">add+1</button>
</template>
導入的指令:
<script setup>
// 導入的指令同樣需要滿足命名規范
import { directive as vClickOutside } from 'v-click-outside'
</script>
<template>
<div v-click-outside />
</template>
更多關于指令,見官方文檔
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
type User={
name: string
age: number
}
// ref
const msg1=ref('') // 會默認約束成 string 類型,因為ts類型推導
const msg2=ref<string>('') // 可以通過范型約束類型
const user1=ref<User>({ name: 'tang', age: 18 }) // 范型約束
const user2=ref({} as User) // 類型斷言
// reactive
const obj=reactive({})
const user3=reactive<User>({ name: 'tang', age: 18 })
const user4=reactive({} as User)
// computed
const msg3=computed(()=> msg1.value)
const user5=computed<User>(()=> {
return { name: 'tang', age: 18 }
})
</script>
此語法諸多的特性,使單個文件組件更簡單!只需要給 script 標簽添加一個 setup 屬性,那么整個 script 就直接會變成setup函數,所有頂級變量、函數,均會自動暴露給模板使用(無需再一個個 return了),開發效率將大大的提高!
以至于連尤大也在微博上呼吁大家:“如果你能用Vue3卻還在用 Options API,現在有了< script setup>沒有理由不換 Composition API了”
前言
JavaScript在百度一直有著廣泛的應用,特別是在瀏覽器端的行為管理。本文檔的目標是使JavaScript代碼風格保持一致,容易被理解和被維護。
雖然本文檔是針對JavaScript設計的,但是在使用各種JavaScript的預編譯語言時(如TypeScript等)時,適用的部分也應盡量遵循本文檔的約定。
2 代碼風格
2.1 文件
[建議] JavaScript 文件使用無 BOM 的 UTF-8 編碼。
解釋:UTF-8 編碼具有更廣泛的適應性。BOM 在使用程序或工具處理文件時可能造成不必要的干擾。
[建議] 在文件結尾處,保留一個空行。
2.2 結構
[強制] 使用 4 個空格做為一個縮進層級,不允許使用 2 個空格 或 tab 字符。
[強制] switch 下的 case 和 default 必須增加一個縮進層級。
// good switch (variable) { case '1': // do... break; case '2': // do... break; default: // do... } // bad switch (variable) { case '1': // do... break; case '2': // do... break; default: // do... }
[強制] 二元運算符兩側必須有一個空格,一元運算符與操作對象之間不允許有空格。
var a=!arr.length; a++; a=b + c;
[強制] 用作代碼塊起始的左花括號 { 前必須有一個空格。
示例:
// good if (condition) { } while (condition) { } function funcName() { } // bad if (condition){ } while (condition){ } function funcName(){ }
[強制] if / else / for / while / function / switch / do / try / catch / finally 關鍵字后,必須有一個空格。
// good if (condition) { } while (condition) { } (function () { })(); // bad if(condition) { } while(condition) { } (function() { })();
[強制] 在對象創建時,屬性中的 : 之后必須有空格,: 之前不允許有空格。
// good var obj={ a: 1, b: 2, c: 3 }; // bad var obj={ a : 1, b:2, c :3 };
[強制] 函數聲明、具名函數表達式、函數調用中,函數名和 ( 之間不允許有空格。
// good function funcName() { } var funcName=function funcName() { }; funcName(); // bad function funcName () { } var funcName=function funcName () { }; funcName ();
[強制] , 和 ; 前不允許有空格。
// good callFunc(a, b); // bad callFunc(a , b) ;
[強制] 在函數調用、函數聲明、括號表達式、屬性訪問、if / for / while / switch / catch 等語句中,() 和 [] 內緊貼括號部分不允許有空格。
// good callFunc(param1, param2, param3); save(this.list[this.indexes[i]]); needIncream && (variable +=increament); if (num > list.length) { } while (len--) { } // bad callFunc( param1, param2, param3 ); save( this.list[ this.indexes[ i ] ] ); needIncreament && ( variable +=increament ); if ( num > list.length ) { } while ( len-- ) { }
[強制] 單行聲明的數組與對象,如果包含元素,{} 和 [] 內緊貼括號部分不允許包含空格。
解釋:聲明包含元素的數組與對象,只有當內部元素的形式較為簡單時,才允許寫在一行。元素復雜的情況,還是應該換行書寫。
// good var arr1=[]; var arr2=[1, 2, 3]; var obj1={}; var obj2={name: 'obj'}; var obj3={ name: 'obj', age: 20, sex: 1 }; // bad var arr1=[ ]; var arr2=[ 1, 2, 3 ]; var obj1={ }; var obj2={ name: 'obj' }; var obj3={name: 'obj', age: 20, sex: 1};
[強制] 行尾不得有多余的空格。
[強制] 每個獨立語句結束后必須換行。
[強制] 每行不得超過 120 個字符。
解釋:超長的不可分割的代碼允許例外,比如復雜的正則表達式。長字符串不在例外之列。
[強制] 運算符處換行時,運算符必須在新行的行首。
// good if (user.isAuthenticated() && user.isInRole('admin') && user.hasAuthority('add-admin') || user.hasAuthority('delete-admin') ) { // Code } var result=number1 + number2 + number3 + number4 + number5; // bad if (user.isAuthenticated() && user.isInRole('admin') && user.hasAuthority('add-admin') || user.hasAuthority('delete-admin')) { // Code } var result=number1 + number2 + number3 + number4 + number5;
[強制] 在函數聲明、函數表達式、函數調用、對象創建、數組創建、for語句等場景中,不允許在 , 或 ; 前換行。
// good var obj={ a: 1, b: 2, c: 3 }; foo( aVeryVeryLongArgument, anotherVeryLongArgument, callback ); // bad var obj={ a: 1 , b: 2 , c: 3 }; foo( aVeryVeryLongArgument , anotherVeryLongArgument , callback );
[建議] 不同行為或邏輯的語句集,使用空行隔開,更易閱讀。
// 僅為按邏輯換行的示例,不代表setStyle的最優實現 function setStyle(element, property, value) { if (element==null) { return; } element.style[property]=value; }
[建議] 在語句的行長度超過 120 時,根據邏輯條件合理縮進。
// 較復雜的邏輯條件組合,將每個條件獨立一行,邏輯運算符放置在行首進行分隔,或將部分邏輯按邏輯組合進行分隔。 // 建議最終將右括號 ) 與左大括號 { 放在獨立一行,保證與 if 內語句塊能容易視覺辨識。 if (user.isAuthenticated() && user.isInRole('admin') && user.hasAuthority('add-admin') || user.hasAuthority('delete-admin') ) { // Code } // 按一定長度截斷字符串,并使用 + 運算符進行連接。 // 分隔字符串盡量按語義進行,如不要在一個完整的名詞中間斷開。 // 特別的,對于HTML片段的拼接,通過縮進,保持和HTML相同的結構。 var html='' // 此處用一個空字符串,以便整個HTML片段都在新行嚴格對齊 + '<article>' + '<h1>Title here</h1>' + '<p>This is a paragraph</p>' + '<footer>Complete</footer>' + '</article>'; // 也可使用數組來進行拼接,相對 + 更容易調整縮進。 var html=[ '<article>', '<h1>Title here</h1>', '<p>This is a paragraph</p>', '<footer>Complete</footer>', '</article>' ]; html=html.join(''); // 當參數過多時,將每個參數獨立寫在一行上,并將結束的右括號 ) 獨立一行。 // 所有參數必須增加一個縮進。 foo( aVeryVeryLongArgument, anotherVeryLongArgument, callback ); // 也可以按邏輯對參數進行組合。 // 最經典的是baidu.format函數,調用時將參數分為“模板”和“數據”兩塊 baidu.format( dateFormatTemplate, year, month, date, hour, minute, second ); // 當函數調用時,如果有一個或以上參數跨越多行,應當每一個參數獨立一行。 // 這通常出現在匿名函數或者對象初始化等作為參數時,如setTimeout函數等。 setTimeout( function () { alert('hello'); }, 200 ); order.data.read( 'id=' + me.model.id, function (data) { me.attchToModel(data.result); callback(); }, 300 ); // 鏈式調用較長時采用縮進進行調整。 $('#items') .find('.selected') .highlight() .end(); // 三元運算符由3部分組成,因此其換行應當根據每個部分的長度不同,形成不同的情況。 var result=thisIsAVeryVeryLongCondition ? resultA : resultB; var result=condition ? thisIsAVeryVeryLongResult : resultB; // 數組和對象初始化的混用,嚴格按照每個對象的 { 和結束 } 在獨立一行的風格書寫。 var array=[ { // ... }, { // ... } ];
[建議] 對于 if...else...、try...catch...finally 等語句,推薦使用在 } 號后添加一個換行 的風格,使代碼層次結構更清晰,閱讀性更好。
if (condition) { // some statements; } else { // some statements; } try { // some statements; } catch (ex) { // some statements; }
[強制] 不得省略語句結束的分號。
[強制] 在 if / else / for / do / while 語句中,即使只有一行,也不得省略塊 {...}。
// good if (condition) { callFunc(); } // bad if (condition) callFunc(); if (condition) callFunc();
[強制] 函數定義結束不允許添加分號。
// good function funcName() { } // bad function funcName() { }; // 如果是函數表達式,分號是不允許省略的。 var funcName=function () { };
[強制] IIFE 必須在函數表達式外添加 (,非 IIFE 不得在函數表達式外添加 (。
解釋:IIFE=Immediately-Invoked Function Expression.
額外的 ( 能夠讓代碼在閱讀的一開始就能判斷函數是否立即被調用,進而明白接下來代碼的用途。而不是一直拖到底部才恍然大悟。
// good var task=(function () { // Code return result; })(); var func=function () { }; // bad var task=function () { // Code return result; }(); var func=(function () { });
2.3 命名
下面提到的 Camel命名法:駝峰命名法;Pascal命名法:帕斯卡命名法,又叫大駝峰命名法。
[強制] 變量 使用 Camel命名法。
var loadingModules={};
[強制] 常量 使用 全部字母大寫,單詞間下劃線分隔 的命名方式。
var HTML_ENTITY={};
[強制] 函數 使用 Camel命名法。
function stringFormat(source) { }
[強制] 函數的 參數 使用 Camel命名法。
function hear(theBells) { }
[強制] 類 使用 Pascal命名法。
function TextNode(options) { }
[強制] 類的 方法 / 屬性 使用 Camel命名法。
function TextNode(value, engine) { this.value=value; this.engine=engine; } TextNode.prototype.clone=function () { return this; };
[強制] 枚舉變量 使用 Pascal命名法,枚舉的屬性 使用 全部字母大寫,單詞間下劃線分隔 的命名方式。
var TargetState={ READING: 1, READED: 2, APPLIED: 3, READY: 4 };
[強制] 命名空間 使用 Camel命名法。
equipments.heavyWeapons={};
[強制] 由多個單詞組成的縮寫詞,在命名中,根據當前命名法和出現的位置,所有字母的大小寫與首字母的大小寫保持一致。
function XMLParser() { } function insertHTML(element, html) { } var httpRequest=new HTTPRequest();
[強制] 類名 使用 名詞。
function Engine(options) { }
[建議] 函數名 使用 動賓短語。
function getStyle(element) { }
[建議] boolean 類型的變量使用 is 或 has 開頭。
var isReady=false; var hasMoreCommands=false;
[建議] Promise對象 用 動賓短語的進行時 表達。
var loadingData=ajax.get('url'); loadingData.then(callback);
2.4 注釋
2.4.1 單行注釋
[強制] 必須獨占一行。// 后跟一個空格,縮進與下一行被注釋說明的代碼一致。
2.4.2 多行注釋
[建議] 避免使用 /*...*/ 這樣的多行注釋。有多行注釋內容時,使用多個單行注釋。
2.4.3 文檔化注釋
[強制] 為了便于代碼閱讀和自文檔化,以下內容必須包含以 /**...*/ 形式的塊注釋中。
解釋:
[強制] 文檔注釋前必須空一行。
[建議] 自文檔化的文檔說明 what,而不是 how。
2.4.4 類型定義
[強制] 類型定義都是以{開始, 以}結束。
解釋:常用類型如:{string}, {number}, {boolean}, {Object}, {Function}, {RegExp}, {Array}, {Date}。
類型不僅局限于內置的類型,也可以是自定義的類型。比如定義了一個類 Developer,就可以使用它來定義一個參數和返回值的類型。
[強制] 對于基本類型 {string}, {number}, {boolean},首字母必須小寫。
類型定義 語法示例 解釋 String {string} -- Number {number} -- Boolean {boolean} -- Object {Object} -- Function {Function} -- RegExp {RegExp} -- Array {Array} -- Date {Date} -- 單一類型集合 {Array.<string>} string 類型的數組 多類型 {(number|boolean)} 可能是 number 類型, 也可能是 boolean 類型 允許為null {?number} 可能是 number, 也可能是 null 不允許為null {!Object} Object 類型, 但不是 null Function類型 {function(number, boolean)} 函數, 形參類型 Function帶返回值 {function(number, boolean):string} 函數, 形參, 返回值類型 參數可選 @param {string=} name 可選參數,=為類型后綴 可變參數 @param {...number} args 變長參數, ...為類型前綴 任意類型 {*} 任意類型 可選任意類型 @param {*=} name 可選參數,類型不限 可變任意類型 @param {...*} args 變長參數,類型不限 2.4.5 文件注釋
[強制] 文件頂部必須包含文件注釋,用 @file 標識文件說明。
/** * @file Describe the file */
[建議] 文件注釋中可以用 @author 標識開發者信息。
解釋:
開發者信息能夠體現開發人員對文件的貢獻,并且能夠讓遇到問題或希望了解相關信息的人找到維護人。通常情況文件在被創建時標識的是創建者。隨著項目的進展,越來越多的人加入,參與這個文件的開發,新的作者應該被加入 @author 標識。
@author 標識具有多人時,原則是按照 責任 進行排序。通常的說就是如果有問題,就是找第一個人應該比找第二個人有效。比如文件的創建者由于各種原因,模塊移交給了其他人或其他團隊,后來因為新增需求,其他人在新增代碼時,添加 @author 標識應該把自己的名字添加在創建人的前面。
@author 中的名字不允許被刪除。任何勞動成果都應該被尊重。
業務項目中,一個文件可能被多人頻繁修改,并且每個人的維護時間都可能不會很長,不建議為文件增加 @author 標識。通過版本控制系統追蹤變更,按業務邏輯單元確定模塊的維護責任人,通過文檔與wiki跟蹤和查詢,是更好的責任管理方式。
對于業務邏輯無關的技術型基礎項目,特別是開源的公共項目,應使用 @author 標識。
/** * @file Describe the file * @author author-name(mail-name@domain.com) * author-name2(mail-name2@domain.com) */
2.4.6 命名空間注釋
[建議] 命名空間使用 @namespace 標識。
/** * @namespace */ var util={};
2.4.7 類注釋
[建議] 使用 @class 標記類或構造函數。
解釋:對于使用對象 constructor 屬性來定義的構造函數,可以使用 @constructor 來標記。
/** * 描述 * * @class */ function Developer() { // constructor body }
[建議] 使用 @extends 標記類的繼承信息。
/** * 描述 * * @class * @extends Developer */ function Fronteer() { Developer.call(this); // constructor body } util.inherits(Fronteer, Developer);
[強制] 使用包裝方式擴展類成員時, 必須通過 @lends 進行重新指向。
解釋:沒有 @lends 標記將無法為該類生成包含擴展類成員的文檔。
/** * 類描述 * * @class * @extends Developer */ function Fronteer() { Developer.call(this); // constructor body } util.extend( Fronteer.prototype, /** @lends Fronteer.prototype */{ _getLevel: function () { // TODO } } );
[強制] 類的屬性或方法等成員信息使用 @public / @protected / @private 中的任意一個,指明可訪問性。
解釋:生成的文檔中將有可訪問性的標記,避免用戶直接使用非 public 的屬性或方法。
/** * 類描述 * * @class * @extends Developer */ var Fronteer=function () { Developer.call(this); /** * 屬性描述 * * @type {string} * @private */ this._level='T12'; // constructor body }; util.inherits(Fronteer, Developer); /** * 方法描述 * * @private * @return {string} 返回值描述 */ Fronteer.prototype._getLevel=function () { };
2.4.8 函數/方法注釋
[強制] 函數/方法注釋必須包含函數說明,有參數和返回值時必須使用注釋標識。
[強制] 參數和返回值注釋必須包含類型信息和說明。
[建議] 當函數是內部函數,外部不可訪問時,可以使用 @inner 標識。
/** * 函數描述 * * @param {string} p1 參數1的說明 * @param {string} p2 參數2的說明,比較長 * 那就換行了. * @param {number=} p3 參數3的說明(可選) * @return {Object} 返回值描述 */ function foo(p1, p2, p3) { var p3=p3 || 10; return { p1: p1, p2: p2, p3: p3 }; }
[強制] 對 Object 中各項的描述, 必須使用 @param 標識。
/** * 函數描述 * * @param {Object} option 參數描述 * @param {string} option.url option項描述 * @param {string=} option.method option項描述,可選參數 */ function foo(option) { // TODO }
[建議] 重寫父類方法時, 應當添加 @override 標識。如果重寫的形參個數、類型、順序和返回值類型均未發生變化,可省略 @param、@return,僅用 @override 標識,否則仍應作完整注釋。
解釋:簡而言之,當子類重寫的方法能直接套用父類的方法注釋時可省略對參數與返回值的注釋。
2.4.9 事件注釋
[強制] 必須使用 @event 標識事件,事件參數的標識與方法描述的參數標識相同。
/** * 值變更時觸發 * * @event * @param {Object} e e描述 * @param {string} e.before before描述 * @param {string} e.after after描述 */ onchange: function (e) { }
[強制] 在會廣播事件的函數前使用 @fires 標識廣播的事件,在廣播事件代碼前使用 @event 標識事件。
[建議] 對于事件對象的注釋,使用 @param 標識,生成文檔時可讀性更好。
/** * 點擊處理 * * @fires Select#change * @private */ Select.prototype.clickHandler=function () { /** * 值變更時觸發 * * @event Select#change * @param {Object} e e描述 * @param {string} e.before before描述 * @param {string} e.after after描述 */ this.fire( 'change', { before: 'foo', after: 'bar' } ); };
2.4.10 常量注釋
[強制] 常量必須使用 @const 標記,并包含說明和類型信息。
/** * 常量說明 * * @const * @type {string} */ var REQUEST_URL='myurl.do';
2.4.11 復雜類型注釋
[建議] 對于類型未定義的復雜結構的注釋,可以使用 @typedef 標識來定義。
// `namespaceA~` 可以換成其它 namepaths 前綴,目的是為了生成文檔中能顯示 `@typedef` 定義的類型和鏈接。 /** * 服務器 * * @typedef {Object} namespaceA~Server * @property {string} host 主機 * @property {number} port 端口 */ /** * 服務器列表 * * @type {Array.<namespaceA~Server>} */ var servers=[ { host: '1.2.3.4', port: 8080 }, { host: '1.2.3.5', port: 8081 } ];
2.4.12 AMD 模塊注釋
[強制] AMD 模塊使用 @module 或 @exports 標識。
解釋:@exports 與 @module 都可以用來標識模塊,區別在于 @module 可以省略模塊名稱。而只使用 @exports 時在 namepaths 中可以省略 module: 前綴。
define( function (require) { /** * foo description * * @exports Foo */ var foo={ // TODO }; /** * baz description * * @return {boolean} return description */ foo.baz=function () { // TODO }; return foo; } );
也可以在 exports 變量前使用 @module 標識:
define( function (require) { /** * module description. * * @module foo */ var exports={}; /** * bar description * */ exports.bar=function () { // TODO }; return exports; } );
如果直接使用 factory 的 exports 參數,還可以:
/** * module description. * * @module */ define( function (require, exports) { /** * bar description * */ exports.bar=function () { // TODO }; return exports; } );
[強制] 對于已使用 @module 標識為 AMD模塊 的引用,在 namepaths 中必須增加 module: 作前綴。
解釋:namepaths 沒有 module: 前綴時,生成的文檔中將無法正確生成鏈接。
/** * 點擊處理 * * @fires module:Select#change * @private */ Select.prototype.clickHandler=function () { /** * 值變更時觸發 * * @event module:Select#change * @param {Object} e e描述 * @param {string} e.before before描述 * @param {string} e.after after描述 */ this.fire( 'change', { before: 'foo', after: 'bar' } ); };
[建議] 對于類定義的模塊,可以使用 @alias 標識構建函數。
/** * A module representing a jacket. * @module jacket */ define( function () { /** * @class * @alias module:jacket */ var Jacket=function () { }; return Jacket; } );
[建議] 多模塊定義時,可以使用 @exports 標識各個模塊。
// one module define('html/utils', /** * Utility functions to ease working with DOM elements. * @exports html/utils */ function () { var exports={ }; return exports; } ); // another module define('tag', /** @exports tag */ function () { var exports={ }; return exports; } );
[建議] 對于 exports 為 Object 的模塊,可以使用@namespace標識。
解釋:使用 @namespace 而不是 @module 或 @exports 時,對模塊的引用可以省略 module: 前綴。
[建議] 對于 exports 為類名的模塊,使用 @class 和 @exports 標識。
// 只使用 @class Bar 時,類方法和屬性都必須增加 @name Bar#methodName 來標識,與 @exports 配合可以免除這一麻煩,并且在引用時可以省去 module: 前綴。 // 另外需要注意類名需要使用 var 定義的方式。 /** * Bar description * * @see foo * @exports Bar * @class */ var Bar=function () { // TODO }; /** * baz description * * @return {(string|Array)} return description */ Bar.prototype.baz=function () { // TODO };
2.4.13 細節注釋
對于內部實現、不容易理解的邏輯說明、摘要信息等,我們可能需要編寫細節注釋。
[建議] 細節注釋遵循單行注釋的格式。說明必須換行時,每行是一個單行注釋的起始。
function foo(p1, p2, opt_p3) { // 這里對具體內部邏輯進行說明 // 說明太長需要換行 for (...) { .... } }
[強制] 有時我們會使用一些特殊標記進行說明。特殊標記必須使用單行注釋的形式。下面列舉了一些常用標記:
解釋:
3 語言特性
3.1 變量
[強制] 變量在使用前必須通過 var 定義。
解釋:不通過 var 定義變量將導致變量污染全局環境。
// good var name='MyName'; // bad name='MyName';
[強制] 每個 var 只能聲明一個變量。
解釋:一個 var 聲明多個變量,容易導致較長的行長度,并且在修改時容易造成逗號和分號的混淆。
// good var hangModules=[]; var missModules=[]; var visited={}; // bad var hangModules=[], missModules=[], visited={};
[強制] 變量必須 即用即聲明,不得在函數或其它形式的代碼塊起始位置統一聲明所有變量。
解釋: 變量聲明與使用的距離越遠,出現的跨度越大,代碼的閱讀與維護成本越高。雖然JavaScript的變量是函數作用域,還是應該根據編程中的意圖,縮小變量出現的距離空間。
// good function kv2List(source) { var list=[]; for (var key in source) { if (source.hasOwnProperty(key)) { var item={ k: key, v: source[key] }; list.push(item); } } return list; } // bad function kv2List(source) { var list=[]; var key; var item; for (key in source) { if (source.hasOwnProperty(key)) { item={ k: key, v: source[key] }; list.push(item); } } return list; }
3.2 條件
[強制] 在 Equality Expression 中使用類型嚴格的===。僅當判斷 null 或 undefined 時,允許使用==null。
解釋:使用===可以避免等于判斷中隱式的類型轉換。
// good if (age===30) { // ...... } // bad if (age==30) { // ...... }
[建議] 盡可能使用簡潔的表達式。
// 字符串為空 // good if (!name) { // ...... } // bad if (name==='') { // ...... } // 字符串非空 // good if (name) { // ...... } // bad if (name !=='') { // ...... } // 數組非空 // good if (collection.length) { // ...... } // bad if (collection.length > 0) { // ...... } // 布爾不成立 // good if (!notTrue) { // ...... } // bad if (notTrue===false) { // ...... } // null 或 undefined // good if (noValue==null) { // ...... } // bad if (noValue===null || typeof noValue==='undefined') { // ...... }
[建議] 按執行頻率排列分支的順序。
解釋:按執行頻率排列分支的順序好處是:
[建議] 對于相同變量或表達式的多值條件,用 switch 代替 if。
// good switch (typeof variable) { case 'object': // ...... break; case 'number': case 'boolean': case 'string': // ...... break; } // bad var type=typeof variable; if (type==='object') { // ...... } else if (type==='number' || type==='boolean' || type==='string') { // ...... }
[建議] 如果函數或全局中的 else 塊后沒有任何語句,可以刪除 else。
示例:
// good function getName() { if (name) { return name; } return 'unnamed'; } // bad function getName() { if (name) { return name; } else { return 'unnamed'; } }
3.3 循環
[建議] 不要在循環體中包含函數表達式,事先將函數提取到循環體外。
解釋:循環體中的函數表達式,運行過程中會生成循環次數個函數對象。
// good function clicker() { // ...... } for (var i=0, len=elements.length; i < len; i++) { var element=elements[i]; addListener(element, 'click', clicker); } // bad for (var i=0, len=elements.length; i < len; i++) { var element=elements[i]; addListener(element, 'click', function () {}); }
[建議] 對循環內多次使用的不變值,在循環外用變量緩存。
// good var width=wrap.offsetWidth + 'px'; for (var i=0, len=elements.length; i < len; i++) { var element=elements[i]; element.style.width=width; // ...... } // bad for (var i=0, len=elements.length; i < len; i++) { var element=elements[i]; element.style.width=wrap.offsetWidth + 'px'; // ...... }
[建議] 對有序集合進行遍歷時,緩存 length。
解釋:雖然現代瀏覽器都對數組長度進行了緩存,但對于一些宿主對象和老舊瀏覽器的數組對象,在每次 length 訪問時會動態計算元素個數,此時緩存 length 能有效提高程序性能。
for (var i=0, len=elements.length; i < len; i++) { var element=elements[i]; // ...... }
[建議] 對有序集合進行順序無關的遍歷時,使用逆序遍歷。
解釋:逆序遍歷可以節省變量,代碼比較優化。
var len=elements.length; while (len--) { var element=elements[len]; // ...... }
3.4 類型
3.4.1 類型檢測
[建議] 類型檢測優先使用 typeof。對象類型檢測使用 instanceof。null 或 undefined 的檢測使用==null。
// string typeof variable==='string' // number typeof variable==='number' // boolean typeof variable==='boolean' // Function typeof variable==='function' // Object typeof variable==='object' // RegExp variable instanceof RegExp // Array variable instanceof Array // null variable===null // null or undefined variable==null // undefined typeof variable==='undefined'
3.4.2 類型轉換
[建議] 轉換成 string 時,使用 + ''。
// good num + ''; // bad new String(num); num.toString(); String(num);
[建議] 轉換成 number 時,通常使用 +。
// good +str; // bad Number(str);
[建議] string 轉換成 number,要轉換的字符串結尾包含非數字并期望忽略時,使用 parseInt。
var width='200px'; parseInt(width, 10);
[強制] 使用 parseInt 時,必須指定進制。
// good parseInt(str, 10); // bad parseInt(str);
[建議] 轉換成 boolean 時,使用 !!。
var num=3.14; !!num;
[建議] number 去除小數點,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt。
// good var num=3.14; Math.ceil(num); // bad var num=3.14; parseInt(num, 10);
3.5 字符串
[強制] 字符串開頭和結束使用單引號 '。
解釋:
var str='我是一個字符串'; var html='<div class="cls">拼接HTML可以省去雙引號轉義</div>';
[建議] 使用 數組 或 + 拼接字符串。
解釋:
示例:
// 使用數組拼接字符串 var str=[ // 推薦換行開始并縮進開始第一個字符串, 對齊代碼, 方便閱讀. '<ul>', '<li>第一項</li>', '<li>第二項</li>', '</ul>' ].join(''); // 使用 + 拼接字符串 var str2='' // 建議第一個為空字符串, 第二個換行開始并縮進開始, 對齊代碼, 方便閱讀 + '<ul>', + '<li>第一項</li>', + '<li>第二項</li>', + '</ul>';
[建議] 復雜的數據到視圖字符串的轉換過程,選用一種模板引擎。
解釋:使用模板引擎有如下好處:
3.6 對象
[強制] 使用對象字面量 {} 創建新 Object。
// good var obj={}; // bad var obj=new Object();
[強制] 對象創建時,如果一個對象的所有 屬性 均可以不添加引號,則所有 屬性 不得添加引號。
var info={ name: 'someone', age: 28 };
[強制] 對象創建時,如果任何一個 屬性 需要添加引號,則所有 屬性 必須添加 '。
解釋:如果屬性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
// good var info={ 'name': 'someone', 'age': 28, 'more-info': '...' }; // bad var info={ name: 'someone', age: 28, 'more-info': '...' };
[強制] 不允許修改和擴展任何原生對象和宿主對象的原型。
// 以下行為絕對禁止 String.prototype.trim=function () { };
[建議] 屬性訪問時,盡量使用 .。
解釋:屬性名符合 Identifier 的要求,就可以通過 . 來訪問,否則就只能通過 [expr] 方式訪問。
通常在 JavaScript 中聲明的對象,屬性命名是使用 Camel 命名法,用 . 來訪問更清晰簡潔。部分特殊的屬性(比如來自后端的JSON),可能采用不尋常的命名方式,可以通過 [expr] 方式訪問。
info.age; info['more-info'];
[建議] for in 遍歷對象時, 使用 hasOwnProperty 過濾掉原型中的屬性。
var newInfo={}; for (var key in info) { if (info.hasOwnProperty(key)) { newInfo[key]=info[key]; } }
3.7 數組
[強制] 使用數組字面量 [] 創建新數組,除非想要創建的是指定長度的數組。
// good var arr=[]; // bad var arr=new Array();
[強制] 遍歷數組不使用 for in。
解釋:數組對象可能存在數字以外的屬性, 這種情況下 for in 不會得到正確結果.
var arr=['a', 'b', 'c']; arr.other='other things'; // 這里僅作演示, 實際中應使用Object類型 // 正確的遍歷方式 for (var i=0, len=arr.length; i < len; i++) { console.log(i); } // 錯誤的遍歷方式 for (i in arr) { console.log(i); }
[建議] 不因為性能的原因自己實現數組排序功能,盡量使用數組的 sort 方法。
解釋:自己實現的常規排序算法,在性能上并不優于數組默認的 sort 方法。以下兩種場景可以自己實現排序:
[建議] 清空數組使用 .length=0。
3.8 函數
3.8.1 函數長度
[建議] 一個函數的長度控制在 50 行以內。
解釋:將過多的邏輯單元混在一個大函數中,易導致難以維護。一個清晰易懂的函數應該完成單一的邏輯單元。復雜的操作應進一步抽取,通過函數的調用來體現流程。
特定算法等不可分割的邏輯允許例外。
function syncViewStateOnUserAction() { if (x.checked) { y.checked=true; z.value=''; } else { y.checked=false; } if (!a.value) { warning.innerText='Please enter it'; submitButton.disabled=true; } else { warning.innerText=''; submitButton.disabled=false; } } // 直接閱讀該函數會難以明確其主線邏輯,因此下方是一種更合理的表達方式: function syncViewStateOnUserAction() { syncXStateToView(); checkAAvailability(); } function syncXStateToView() { if (x.checked) { y.checked=true; z.value=''; } else { y.checked=false; } } function checkAAvailability() { if (!a.value) { displayWarningForAMissing(); } else { clearWarnignForA(); } }
3.8.2 參數設計
[建議] 一個函數的參數控制在 6 個以內。
解釋:
除去不定長參數以外,函數具備不同邏輯意義的參數建議控制在 6 個以內,過多參數會導致維護難度增大。
某些情況下,如使用 AMD Loader 的 require 加載多個模塊時,其 callback 可能會存在較多參數,因此對函數參數的個數不做強制限制。
[建議] 通過 options 參數傳遞非數據輸入型參數。
解釋:有些函數的參數并不是作為算法的輸入,而是對算法的某些分支條件判斷之用,此類參數建議通過一個 options 參數傳遞。
如下函數:
/** * 移除某個元素 * * @param {Node} element 需要移除的元素 * @param {boolean} removeEventListeners 是否同時將所有注冊在元素上的事件移除 */ function removeElement(element, removeEventListeners) { element.parent.removeChild(element); if (removeEventListeners) { element.clearEventListeners(); } }
可以轉換為下面的簽名:
/** * 移除某個元素 * * @param {Node} element 需要移除的元素 * @param {Object} options 相關的邏輯配置 * @param {boolean} options.removeEventListeners 是否同時將所有注冊在元素上的事件移除 */ function removeElement(element, options) { element.parent.removeChild(element); if (options.removeEventListeners) { element.clearEventListeners(); } }
這種模式有幾個顯著的優勢:
3.8.3 閉包
[建議] 在適當的時候將閉包內大對象置為 null。
解釋:
在 JavaScript 中,無需特別的關鍵詞就可以使用閉包,一個函數可以任意訪問在其定義的作用域外的變量。需要注意的是,函數的作用域是靜態的,即在定義時決定,與調用的時機和方式沒有任何關系。
閉包會阻止一些變量的垃圾回收,對于較老舊的JavaScript引擎,可能導致外部所有變量均無法回收。
首先一個較為明確的結論是,以下內容會影響到閉包內變量的回收:
Chakra、V8 和 SpiderMonkey 將受以上因素的影響,表現出不盡相同又較為相似的回收策略,而JScript.dll和Carakan則完全沒有這方面的優化,會完整保留整個 LexicalEnvironment 中的所有變量綁定,造成一定的內存消耗。
由于對閉包內變量有回收優化策略的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,因此可以總結如下,當返回一個函數 fn 時:
對于Chakra引擎,暫無法得知是按 V8 的模式還是按 SpiderMonkey 的模式進行。
如果有 非常龐大 的對象,且預計會在 老舊的引擎 中執行,則使用閉包時,注意將閉包不需要的對象置為空引用。
[建議] 使用 IIFE 避免 Lift 效應。
解釋:在引用函數外部變量時,函數執行時外部變量的值由運行時決定而非定義時,最典型的場景如下:
var tasks=[]; for (var i=0; i < 5; i++) { tasks[tasks.length]=function () { console.log('Current cursor is at ' + i); }; } var len=tasks.length; while (len--) { tasks[len](); }
以上代碼對 tasks 中的函數的執行均會輸出 Current cursor is at 5,往往不符合預期。
此現象稱為 Lift 效應 。解決的方式是通過額外加上一層閉包函數,將需要的外部變量作為參數傳遞來解除變量的綁定關系:
var tasks=[]; for (var i=0; i < 5; i++) { // 注意有一層額外的閉包 tasks[tasks.length]=(function (i) { return function () { console.log('Current cursor is at ' + i); }; })(i); } var len=tasks.length; while (len--) { tasks[len](); }
3.8.4 空函數
[建議] 空函數不使用 new Function() 的形式。
var emptyFunction=function () {};
[建議] 對于性能有高要求的場合,建議存在一個空函數的常量,供多處使用共享。
var EMPTY_FUNCTION=function () {}; function MyClass() { } MyClass.prototype.abstractMethod=EMPTY_FUNCTION; MyClass.prototype.hooks.before=EMPTY_FUNCTION; MyClass.prototype.hooks.after=EMPTY_FUNCTION;
3.9 面向對象
[強制] 類的繼承方案,實現時需要修正 constructor。
解釋:通常使用其他 library 的類繼承方案都會進行 constructor 修正。如果是自己實現的類繼承方案,需要進行 constructor 修正。
/** * 構建類之間的繼承關系 * * @param {Function} subClass 子類函數 * @param {Function} superClass 父類函數 */ function inherits(subClass, superClass) { var F=new Function(); F.prototype=superClass.prototype; subClass.prototype=new F(); subClass.prototype.constructor=subClass; }
[建議] 聲明類時,保證 constructor 的正確性。
function Animal(name) { this.name=name; } // 直接prototype等于對象時,需要修正constructor Animal.prototype={ constructor: Animal, jump: function () { alert('animal ' + this.name + ' jump'); } }; // 這種方式擴展prototype則無需理會constructor Animal.prototype.jump=function () { alert('animal ' + this.name + ' jump'); };
[建議] 屬性在構造函數中聲明,方法在原型中聲明。
解釋: 原型對象的成員被所有實例共享,能節約內存占用。所以編碼時我們應該遵守這樣的原則:原型對象包含程序不會修改的成員,如方法函數或配置項。
function TextNode(value, engine) { this.value=value; this.engine=engine; } TextNode.prototype.clone=function () { return this; };
[強制] 自定義事件的 事件名 必須全小寫。
解釋:在 JavaScript 廣泛應用的瀏覽器環境,絕大多數 DOM 事件名稱都是全小寫的。為了遵循大多數 JavaScript 開發者的習慣,在設計自定義事件時,事件名也應該全小寫。
[強制] 自定義事件只能有一個 event 參數。如果事件需要傳遞較多信息,應仔細設計事件對象。
解釋:一個事件對象的好處有:
[建議] 設計自定義事件時,應考慮禁止默認行為。
解釋:常見禁止默認行為的方式有兩種:
3.10 動態特性
3.10.1 eval
[強制] 避免使用直接 eval 函數。
解釋:直接 eval,指的是以函數方式調用 eval 的調用方法。直接 eval 調用執行代碼的作用域為本地作用域,應當避免。
如果有特殊情況需要使用直接 eval,需在代碼中用詳細的注釋說明為何必須使用直接 eval,不能使用其它動態執行代碼的方式,同時需要其他資深工程師進行 Code Review。
[建議] 盡量避免使用 eval 函數。
3.10.2 動態執行代碼
[建議] 使用 new Function 執行動態代碼。
解釋:通過 new Function 生成的函數作用域是全局使用域,不會影響當當前的本地作用域。如果有動態代碼執行的需求,建議使用 new Function。
var handler=new Function('x', 'y', 'return x + y;'); var result=handler($('#x').val(), $('#y').val());
3.10.3 with
[建議] 盡量不要使用 with。
解釋:使用 with 可能會增加代碼的復雜度,不利于閱讀和管理;也會對性能有影響。大多數使用 with 的場景都能使用其他方式較好的替代。所以,盡量不要使用 with。
3.10.4 delete
[建議] 減少 delete 的使用。
解釋:如果沒有特別的需求,減少或避免使用delete。delete的使用會破壞部分 JavaScript 引擎的性能優化。
[建議] 處理 delete 可能產生的異常。
解釋:
對于有被遍歷需求,且值 null 被認為具有業務邏輯意義的值的對象,移除某個屬性必須使用 delete 操作。
在嚴格模式或IE下使用 delete 時,不能被刪除的屬性會拋出異常,因此在不確定屬性是否可以刪除的情況下,建議添加 try-catch 塊。
try { delete o.x; } catch (deleteError) { o.x=null; }
3.10.5 對象屬性
[建議] 避免修改外部傳入的對象。
解釋:
JavaScript 因其腳本語言的動態特性,當一個對象未被 seal 或 freeze 時,可以任意添加、刪除、修改屬性值。
但是隨意地對 非自身控制的對象 進行修改,很容易造成代碼在不可預知的情況下出現問題。因此,設計良好的組件、函數應該避免對外部傳入的對象的修改。
下面代碼的 selectNode 方法修改了由外部傳入的 datasource 對象。如果 datasource 用在其它場合(如另一個 Tree 實例)下,會造成狀態的混亂。
function Tree(datasource) { this.datasource=datasource; } Tree.prototype.selectNode=function (id) { // 從datasource中找出節點對象 var node=this.findNode(id); if (node) { node.selected=true; this.flushView(); } };
對于此類場景,需要使用額外的對象來維護,使用由自身控制,不與外部產生任何交互的 selectedNodeIndex 對象來維護節點的選中狀態,不對 datasource 作任何修改。
function Tree(datasource) { this.datasource=datasource; this.selectedNodeIndex={}; } Tree.prototype.selectNode=function (id) { // 從datasource中找出節點對象 var node=this.findNode(id); if (node) { this.selectedNodeIndex[id]=true; this.flushView(); } };
除此之外,也可以通過 deepClone 等手段將自身維護的對象與外部傳入的分離,保證不會相互影響。
[建議] 具備強類型的設計。
解釋:
4 瀏覽器環境
4.1 模塊化
4.1.1 AMD
[強制] 使用 AMD 作為模塊定義。
解釋:
AMD 作為由社區認可的模塊定義形式,提供多種重載提供靈活的使用方式,并且絕大多數優秀的 Library 都支持 AMD,適合作為規范。
目前,比較成熟的 AMD Loader 有:
[強制] 模塊 id 必須符合標準。
解釋:模塊 id 必須符合以下約束條件:
4.1.2 define
[建議] 定義模塊時不要指明 id 和 dependencies。
解釋:
在 AMD 的設計思想里,模塊名稱是和所在路徑相關的,匿名的模塊更利于封包和遷移。模塊依賴應在模塊定義內部通過 local require 引用。
所以,推薦使用 define(factory) 的形式進行模塊定義。
define( function (require) { } );
[建議] 使用 return 來返回模塊定義。
解釋:使用 return 可以減少 factory 接收的參數(不需要接收 exports 和 module),在沒有 AMD Loader 的場景下也更容易進行簡單的處理來偽造一個 Loader。
define( function (require) { var exports={}; // ... return exports; } );
4.1.3 require
[強制] 全局運行環境中,require 必須以 async require 形式調用。
解釋:模塊的加載過程是異步的,同步調用并無法保證得到正確的結果。
// good require(['foo'], function (foo) { }); // bad var foo=require('foo');
[強制] 模塊定義中只允許使用 local require,不允許使用 global require。
解釋:
[強制] Package在實現時,內部模塊的 require 必須使用 relative id。
解釋:對于任何可能通過 發布-引入 的形式復用的第三方庫、框架、包,開發者所定義的名稱不代表使用者使用的名稱。因此不要基于任何名稱的假設。在實現源碼中,require 自身的其它模塊時使用 relative id。
define( function (require) { var util=require('./util'); } );
[建議] 不會被調用的依賴模塊,在 factory 開始處統一 require。
解釋:有些模塊是依賴的模塊,但不會在模塊實現中被直接調用,最為典型的是 css / js / tpl 等 Plugin 所引入的外部內容。此類內容建議放在模塊定義最開始處統一引用。
define( function (require) { require('css!foo.css'); require('tpl!bar.tpl.html'); // ... } );
4.2 DOM
4.2.1 元素獲取
[建議] 對于單個元素,盡可能使用 document.getElementById 獲取,避免使用document.all。
[建議] 對于多個元素的集合,盡可能使用 context.getElementsByTagName 獲取。其中 context 可以為 document 或其他元素。指定 tagName 參數為 * 可以獲得所有子元素。
[建議] 遍歷元素集合時,盡量緩存集合長度。如需多次操作同一集合,則應將集合轉為數組。
解釋:原生獲取元素集合的結果并不直接引用 DOM 元素,而是對索引進行讀取,所以 DOM 結構的改變會實時反映到結果中。
<div></div> <span></span> <script> var elements=document.getElementsByTagName('*'); // 顯示為 DIV alert(elements[0].tagName); var div=elements[0]; var p=document.createElement('p'); document.body.insertBefore(p, div); // 顯示為 P alert(elements[0].tagName); </script>
[建議] 獲取元素的直接子元素時使用 children。避免使用childNodes,除非預期是需要包含文本、注釋和屬性類型的節點。
4.2.2 樣式獲取
[建議] 獲取元素實際樣式信息時,應使用 getComputedStyle 或 currentStyle。
解釋:通過 style 只能獲得內聯定義或通過 JavaScript 直接設置的樣式。通過 CSS class 設置的元素樣式無法直接通過 style 獲取。
4.2.3 樣式設置
[建議] 盡可能通過為元素添加預定義的 className 來改變元素樣式,避免直接操作 style 設置。
[強制] 通過 style 對象設置元素樣式時,對于帶單位非 0 值的屬性,不允許省略單位。
解釋:除了 IE,標準瀏覽器會忽略不規范的屬性值,導致兼容性問題。
4.2.4 DOM 操作
[建議] 操作 DOM 時,盡量減少頁面 reflow。
解釋:頁面 reflow 是非常耗時的行為,非常容易導致性能瓶頸。下面一些場景會觸發瀏覽器的reflow:
[建議] 盡量減少 DOM 操作。
解釋:DOM 操作也是非常耗時的一種操作,減少 DOM 操作有助于提高性能。舉一個簡單的例子,構建一個列表。我們可以用兩種方式:
第一種方法看起來比較標準,但是每次循環都會對 DOM 進行操作,性能極低。在這里推薦使用第二種方法。
4.2.5 DOM 事件
[建議] 優先使用 addEventListener / attachEvent 綁定事件,避免直接在 HTML 屬性中或 DOM 的 expando 屬性綁定事件處理。
解釋:expando 屬性綁定事件容易導致互相覆蓋。
[建議] 使用 addEventListener 時第三個參數使用 false。
解釋:標準瀏覽器中的 addEventListener 可以通過第三個參數指定兩種時間觸發模型:冒泡和捕獲。而 IE 的 attachEvent 僅支持冒泡的事件觸發。所以為了保持一致性,通常 addEventListener 的第三個參數都為 false。
[建議] 在沒有事件自動管理的框架支持下,應持有監聽器函數的引用,在適當時候(元素釋放、頁面卸載等)移除添加的監聽器。
作者:前端切圖小弟,個人運營的公眾號:前端讀者(fe_duzhe)
*請認真填寫需求信息,我們會在24小時內與您取得聯系。