我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象: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 只能重定义属性的读取(get)和设置(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小时内与您取得联系。