Vue3 脚本设置语法糖详细信息
最后更新于 2023.07.13
目前setup sugar
已经进行了定稿,vue3 + setup sugar + TS
的写法看起来很香,写本文时 Vue 版本是 "^3.2.6"
script setup 语法糖
新的 setup
选项是在组件创建之前, props
被解析之后执行,是组合式 API 的入口。
WARNING
在setup
中你应该避免使用this
,因为它不会找到组件实例。setup
的调用发生在data
property、computed
property 或methods
被解析之前,所以它们无法>在setup
中被获取。
setup
选项是一个接收 props
和 context
的函数,我们将在之后进行讨论。此外,我们将 setup
返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
它是 Vue3 的一个新语法糖,在 setup
函数中。所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。
在添加了setup的script标签中,我们不必声明和方法,这种写法会自动将所有*变量、函数,均会自动暴露给模板(template)使用
这里强调一句 “暴露给模板,跟暴露给外部不是一回事”
使用方式极其简单,仅需要在 script
标签加上 setup
关键字即可。示例:
<script setup></script>
该setup功能是新的组件选项。它是组件内部暴露出所有的属性和方法的统一API。
使用后意味着,script标签内的内容相当于原本组件声明中setup()的函数体,不过也有一定的区别。
使用 script setup 语法糖,组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。基本语法
调用时机
创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用.
模板中使用
如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
<template>
<div>
{{ count }} {{ object.foo }}
</div>
</template>
setup 参数
- 「props」 第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。props和vue2.x并无什么不同,仍然遵循以前的原则;
- 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错。
- 不要结构props。结构的props会失去响应性。
2.「context」 第二个参数提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property。
<script setup="props, context" lang="ts">
context.attrs
context.slots
context.emit
<script>
像这样,只要在setup处声明即可自动导入,同时也支持解构语法:
<script setup="props, { emit }" lang="ts">
<script>
组件自动注册
导入 component 或 directive 直接import即可,无需额外声明
import { MyButton } from "@/components"
import { directive as clickOutside } from 'v-click-outside'
与原先一样,模板中也支持使用kabab-case来创建组件,如
在 script setup 中,引入的组件可以直接使用,无需再通过components
进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name
属性了。示例:
<template>
<HelloWorld />
</template>
<script setup>
import HelloWorld from "./components/HelloWorld.vue"; //此处使用 Vetur 插件会报红
</script>
如果需要定义类似 name 的属性,可以再加个平级的 script 标签,在里面实现即可。
组件核心 API 的使用
定义组件的 props
通过defineProps
指定当前 props 类型,获得上下文的props对象。示例:
<script setup lang="ts">
import { defineProps } from 'vue';
const props = defineProps(["title"]);
</script>
<!-- 或者 -->
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
title: String, // 可以设置传来值的类型
})
</script>
<!-- 或者 -->
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
// 可以设置传来值的类型和默认值
title: {
type:String,
default: ''
}
})
</script>
<!-- 或者 -->
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
title: [String,Number] // 可以设置传来值的多种类型
})
</script>
<!-- 或者 -->
<script setup lang="ts">
import { ref,defineProps } from 'vue';
type Props={
title:string
}
defineProps<Props>();
</script>
定义 emit
使用defineEmit
定义当前组件含有的事件,并通过返回的上下文去执行 emit。示例:
<script setup>
import { defineEmits } from 'vue'
// 定义两个方法 change 和 delete
const emit = defineEmits(['change', 'delete'])
const handleClick=()=>{
emit('change', 5); // 传给父组件的值的方法
}
const handleClickDel=()=>{
emit('delete', 8); // 传给父组件的值的方法
}
</script>
父子组件通信
- 在Vue3中父子组件传值、方法是通过
defineProps
,defineEmits
实现的。 -
defineProps
和defineEmits
都是只在<script setup>
中才能使用的。 - 它们不需要导入就会随着
<script setup>
被一同处理编译。 -
defineProps
接收与props
选项相同的值,defineEmits
也接收emits
选项 相同的值。
defineProps 用来接收父组件传来的 props ; defineEmits 用来声明触发的事件。
//父组件
<template>
<my-son foo="????????????????????????" @childClick="childClick" />
</template>
<script lang="ts" setup>
import MySon from "./MySon.vue";
let childClick = (e: any):void => {
console.log('from son:',e); //????????????????????????
};
</script>
//子组件
<template>
<span @click="sonToFather">信息:{{ props.foo }}</span>
</template>
<script lang="ts" setup>
import { defineEmits, defineProps} from "vue";
const emit = defineEmits(["childClick"]); // 声明触发事件 childClick
const props = defineProps({ foo: String }); // 获取props
const sonToFather = () =>{
emit('childClick' , props.foo)
}
</script>
子组件通过 defineProps 接收父组件传过来的数据,子组件通过 defineEmits 定义事件发送信息给父组件
增强的props类型定义:
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmit<(e: 'update' | 'delete', id: number) => void>()
不过注意,采用这种方法将无法使用props默认值。
父组件调用子组件事件
父组件
<!-- 父组件 app.vue -->
<template>
<div class="par">
<!-- 使用 ref 指令关联子组件 -->
<child ref="childRef"/>
<button @click="handleClick">提交</button>
</div>
</template>
<script setup>
import { reactive, ref } from "vue";
import child from "./child.vue";
//定义子组件实例,名称要和上面的ref相同
const childRef = ref(null);
//访问demo组件的方法或对象
const handleClick = () => {
//获取到子组件的 title 数据
let title = childRef.value.state.title;
//调用子组件的 play方法
childRef.value.submit();
};
</script>
子组件
子组件通过defineExpose暴露对象和方法
<!--子组件名称 child.vue -->
<template>
<div class="child">
{{ state.title }}
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
//定义一个变量
const state = reactive({
title: "标题",
});
//定义一个方法
const submit = () => {
state.title = "你调用了子组件的方法";
};
//暴露state和play方法
defineExpose({
state,
submit
});
</script>
定义响应变量、函数、监听、计算属性computed
<script setup lang="ts">
import { ref,computed,watchEffect } from 'vue';
const count = ref(0); //不用 return ,直接在 templete 中使用
const addCount=()=>{ //定义函数,使用同上
count.value++;
}
//定义计算属性,使用同上
const howCount=computed(()=>"现在count值为:"+count.value);
//定义监听,使用同上 //...some code else
watchEffect(()=>console.log(count.value));
</script>
watchEffect
用于有副作用的操作,会自动收集依赖。
和watch区别
无需区分deep,immediate,只要依赖的数据发生变化,就会调用
reactive
此时name只会在初次创建的时候进行赋值,如果中间想要改变name的值,那么需要借助composition api 中的reactive。
<script setup lang="ts">
import { reactive, onUnmounted } from 'vue'
const state = reactive({
counter: 0
})
// 定时器 每秒都会更新数据
const timer = setInterval(() => {
state.counter++
}, 1000);
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{state.counter}}</div>
</template>
使用ref也能达到我们预期的'counter',并且在模板中,vue进行了处理,我们可以直接使用counter而不用写counter.value.
ref和reactive的关系:
ref是一个{value:'xxxx'}的结构,value是一个reactive对象
ref 暴露变量到模板
曾经的提案中,如果需要暴露变量到模板,需要在变量前加入export声明:
export const count = ref(0)
不过在新版的提案中,无需export声明,编译器会自动寻找模板中使用的变量,只需像下面这样简单的声明,即可在模板中使用该变量
<script setup lang="ts">
import { ref } from 'vue'
const counter = ref(0);//不用 return ,直接在 templete 中使用
const timer = setInterval(() => {
counter.value++
}, 1000)
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{counter}}</div>
</template>
生命周期方法
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。官网:生命周期钩子
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup
|
---|---|
beforeCreate |
Not needed* |
created |
Not needed* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
activated |
onActivated |
deactivated |
onDeactivated |
<script setup lang="ts">
import { onMounted } from 'vue';
onMounted(() => { console.log('mounted!'); });
</script>
获取 slots 和 attrs
注:useContext API 被弃用,取而代之的是更加细分的 api。
可以通过useContext
从上下文中获取 slots 和 attrs。不过提案在正式通过后,废除了这个语法,被拆分成了useAttrs
和useSlots
。
-
useAttrs
:见名知意,这是用来获取 attrs 数据,但是这和 vue2 不同,里面包含了class
、属性
、方法
。
<template>
<component v-bind='attrs'></component>
</template>
<srcipt setup lang='ts'>
const attrs = useAttrs();
<script>
-
useSlots
: 顾名思义,获取插槽数据。
使用示例:
// 旧
<script setup>
import { useContext } from 'vue'
const { slots, attrs } = useContext()
</script>
// 新
<script setup>
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs()
const slots = useSlots()
</script>
其他 Hook Api
-
useCSSModule
:CSS Modules 是一种 CSS 的模块化和组合系统。vue-loader 集成 CSS Modules,可以作为模拟 scoped CSS。允许在单个文件组件的setup
中访问CSS模块。此 api 本人用的比较少,不过多做介绍。 -
useCssVars
: 此 api 暂时资料比较少。介绍v-bind in styles
时提到过。 -
useTransitionState
: 此 api 暂时资料比较少。 -
useSSRContext
: 此 api 暂时资料比较少。
defineExpose API
传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template
模板,谁都不能访问内部的数据和方法。
如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:
const a = 1
const b = ref(2)
defineExpose({ a, b, })
注意:目前发现
defineExpose
暴露出去的属性以及方法都是unknown
类型,如果有修正类型的方法,欢迎评论区补充。
//父组件
<template>
<Daughter ref="daughter" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Daughter from "./Daughter.vue";
const daughter = ref(null)
console.log('????????????????~daughter',daughter)
</script>
//子组件
<template>
<div>妾身{{ msg }}</div>
</template>
<script lang="ts" setup>
import { ref ,defineExpose} from "vue";
const msg = ref('貂蝉')
defineExpose({
msg
})
</script>
一、父组件调用子组件方法
子组件需要使用defineExpose对外暴露方法,父组件才可以调用。
子组件
<template>
<div>子组件</div>
</template>
<script setup lang="ts">
// 第一步:定义子组件的方法
const handleSubmit = (str: string) => {
console.log('子组件的hello方法执行了--' + str)
}
// 第二部:暴露方法
defineExpose({
handleSubmit
})
</script>
父组件
<template>
<button @click="handleChild">触发子组件方法</button>
<!-- 一:定义 ref -->
<Child ref="childRef"></Child>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Child from '../../components/child.vue';
// 二:定义与 ref 同名变量
const childRef = ref <any> ()
// 三、函数
const handleChild = () => {
// 调用子组件的方法或者变量,通过value
childRef.value.hello("hello world!");
}
</script>
参考网址:cn.vuejs.org/api/sfc-scr…
属性和方法无需返回,直接使用!
这可能是带来的较大便利之一,在以往的写法中,定义数据和方法,都需要在结尾 return 出去,才能在模板中使用。在 script setup 中,定义的属性和方法无需返回,可以直接使用!示例:
<template>
<div>
<p>My name is {{name}}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = ref('Sam')
</script>
支持 async await 异步
注意在vue3的源代码中,setup执行完毕,函数 getCurrentInstance 内部的有个值会释放对 currentInstance 的引用,await 语句会导致后续代码进入异步执行的情况。所以上述例子中最后一个 getCurrentInstance() 会返回 null,建议使用变量保存第一个 getCurrentInstance() 返回的引用.
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
<script setup>
中可以使用顶层 await
。结果代码会被编译成 async setup()
:
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
另外,await 的表达式会自动编译成在 await
之后保留当前组件实例上下文的格式。
注意
async setup()
必须与Suspense
组合使用,Suspense
目前还是处于实验阶段的特性。我们打算在将来的某个发布版本中开发完成并提供文档 - 如果你现在感兴趣,可以参照 tests 看它是如何工作的。
定义组件其他配置
配置项的缺失,有时候我们需要更改组件选项,在setup
中我们目前是无法做到的。我们需要在上方
再引入一个 script
,在上方写入对应的 export
即可,需要单开一个 script。
<script setup>
可以和普通的 <script>
一起使用。普通的 <script>
在有这些需要的情况下或许会被使用到:
- 无法在
<script setup>
声明的选项,例如inheritAttrs
或通过插件启用的自定义的选项。 - 声明命名导出。
- 运行副作用或者创建只需要执行一次的对象。
在script setup 外使用export default,其内容会被处理后放入原组件声明字段。
<script>
// 普通 `<script>`, 在模块范围下执行(只执行一次)
runSideEffectOnce()
// 声明额外的选项
export default {
name: "MyComponent",
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
import HelloWorld from '../components/HelloWorld.vue'
// 在 setup() 作用域中执行 (对每个实例皆如此)
// your code
</script>
<template>
<div>
<HelloWorld msg="Vue3 + TypeScript + Vite"/>
</div>
</template>
注意:Vue 3 SFC 一般会自动从组件的文件名推断出组件的 name。在大多数情况下,不需要明确的 name 声明。唯一需要的情况是当你需要
<keep-alive>
包含或排除或直接检查组件的选项时,你需要这个名字。
关于 TS 与 ESLint 的不完美
-
与
@typescript-eslint/no-unused-vars
规则不兼容,此规则含义为定义了,未进行使用。该规则其实影响不大,关闭即可。 -
与导入的类型声明不兼容,当你通过解构的方式去导入类型,
setup sugar
会进行自动导出。这时候,你就会收到 TS 的一条报错:此为类型,但被当作值使用。解决办法:类型导出使用export default
导出或者引入时使用import * as xx
来进行引入,也可以使用import type { test } from "./test";
解决。
语法糖实现
vue文件代码
<template>
<div>{{ msg }}</div>
</template>
<script setup>
const msg = 'Hello!'
</script>
编译后的js代码:
export default {
setup() {
const msg = 'Hello!'
return function render() {
// has access to everything inside setup() scope
// 在函数 setup 作用域,函数 render 能访问 setup 的一切,
return h('div', msg)
}
}
}
注意到,即使普通变量也能作为模版被置入 template 中被编译,某些人认为这不合适,不够分离。
vscode配套插件
volar 是一个vscode插件,用来增强vue编写体验,使用volar插件可以获得script setup语法的最佳支持。
与vetur
相同,volar
是一个针对vue
的vscode
插件,不过与vetur
不同的是,volar
提供了更为强大的功能,让人直呼卧槽
。
安装的方式很简单,直接在vscode
的插件市场搜索volar
,然后点击安装就可以了。
vscode
中使用的时候,先禁用 Vetur
,再下载使用Volar
使用习惯
从options api
切换到composition api
最大的问题无异于最大的问题就是没有强制的代码分区,如果书写的人没有很好的代码习惯,那么后续的人将会看的十分难受。目前我是这么解决的:
-
自我代码分区并且尽量抽离方法(写好注释),分区如下:
- 相关引入
- 响应式数据、props、emit 定义
- 生命周期以及 watch 书写
- 方法定义
- 方法、属性暴露
-
组件抽离:将页面拆成两个文件夹,一个为
views
,一个为components
。views 和 components 文件夹下有各自的文件。views 文件夹中为页面入口,掌管数据,而 components 则为页面中一些组件抽离。如果是公共组件,再抽离到 components 文件夹下其他位置。 -
hook 抽离:尽可能将逻辑抽离,并不一定要进行复用。
写在最后
写作不易,希望可以获得你的一个「赞」。如果文章对你有用,可以选择「关注 + 收藏」。 如有文章有错误或建议,欢迎评论指正,谢谢你。❤️
推荐阅读
-
vue3 设置语法糖 defineExpose 的用法
-
Vue3 - Foundation 3(组合式 API,设置语法糖)
-
开始使用后,我发现 Vue3 的脚本设置语法真的很酷。
-
Vue3 脚本设置语法糖详细信息
-
Vue3 脚本设置综合分析
-
vue3 新语法糖--设置脚本
-
Vue3 脚本设置语法糖,只问你甜不甜!
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面