前言
这里是前端学习Dart第六节,本节知识点:Dart函数
、Lambda
、词法作用域
、Javascript作用域
、作用域链
、闭包
、This
等。
视频地址:传送门。
函数
函数的概念是可读,可维护和可重用代码的构建块
。函数是一组用于执行特定任务的语句。函数将程序组织成逻辑代码块。一旦定义,就可以调用函数来访问代码。这使得代码可以重用
。此外,函数可以轻松读取和维护程序的代码。
函数声明告诉编译器函数的名称,返回类型和参数。函数定义提供函数的实际主体。
看一下基本函数的声明:
依然是先声明后调用的方式,如果先调用是会报错的。
Error: Method not found: ‘action’.
函数的返回值
与js函数一致,函数是可以返回任意类型数据的,但是Dart中函数是具有返回值类型检查的可以开启。比如下方方式
无返回值函数可以用void
类型检查。
函数的参数
参数是一种将值传递给函数的机制。参数构成函数签名的一部分。参数值在调用期间传递给函数。除非明确指定,否则传递给函数的值的数量必须与定义的参数数量相匹配。
函数的参数声明有几种方式:
- 必须传的参数
在函数调用期间必须将值传递给所需的参数。
可以看到上述函数action
声明了两个传入参数(形参),但是在函数调用的时候只传入了一个参数,因为这样声明函数参数是必须传的参数,所以抛了一个错误。
Error: Too few positional arguments: 2 required, 1 given
- 参数位置可选
要指定可选的位置参数,请使用square 方括号
[]
括号。修改一下上面那个例子:
可以看到age参数被括了起来之后,调用函数的时候就不报错了,可选位置参数未传入的话默认值是null
多个可选位置参数是这样声明的
这里有个注意点:Dart函数中的所有必须参数必须在可选位置参数之前声明
。一定要注意。
可选位置参数可以赋予默认值:
默认值必须是编译时常量 const , final则不行,因为final是运行时常量
- 可选命名参数
与位置参数不同,必须在传递值时指定参数名称。Curly brace 花括号
{}
可用于指定可选的命名参数。
首先这样的声明方式依然是可选的,可传可不传。
但是在传入这个可选命名参数的时候必须使用声明的变量名称
可选命名参数也支持声明时赋予默认值
注意事项:可选命名参数与位置可选参数只能出现一种,不可以两种同时出现。
- 函数作为参数
Dart中支持函数作为参数传入函数。
箭头函数
如果函数只包含一个表达式,那么可以使用箭头方式使用。
=> expr;
语法是 { return expr; }
的缩写
这里有一个注意事项:在箭头与分号之间只能出现表达式,而不能是语句,例如:不可以在那里放置if语句,但是可以使用条件表达式。
可以看到箭头函数是具有返回值的,当然你也可以设置void函数返回类型,这样的话就屏蔽掉了返回值。
箭头函数又称之为Lambda函数
,那么什么是Lambda
呢。
Lambda
可知函数的概念为代码块
。
首先有一个问题,就是要将一个函数赋值给一个变量要怎么赋值呢,在java前面的版本函数值不可以赋值给一个变量的,后来可以了,但是是类似这个样子的:
当然,这个并不是一个很简洁的写法。所以,为了使这个赋值操作更加优雅, 我们可以移除一些没用的声明,就开始了如下演变
这样,我们就成功的非常优雅的把一块代码
赋给了一个变量
。而这块代码
,或者说这个被赋给一个变量的函数
,就是一个Lambda表达式
。
诶,如果上述最后转换成的样子叫做表达式的话,右侧的匿名函数
就是Lambda
。
匿名函数:
大多数函数都是命名的,例如main()
、print()
等,我们还可以创建一个无名函数,称为匿名函数,有时候也称为labmbda
或者闭包
。看一下下面的例子
作用域
Dart的函数作用域就是词法作用域
Lexical scoping
。为静态作用域
。
无论函数在哪里调用,也无论它如何被调用,他的词法作用域都只有函数被声明时所处的位置决定。
所谓词法作用域就是这样的一个静态的作用域模型,由全局作用域函数创建的作用域和块作用域等组成。
一个 Lexical scoping
内部 是能够访问到 外部 Lexical scoping
中定义的变量的。
这里出现了未定义该变量的错误警告,可以看出 printName 中定义的变量,对于 main 函数中的变量是不可见的。Dart 和 JavaScript 一样具有链式作用域
,也就是说,子作用域可以访问父(甚至是祖先)作用域中的变量,而反过来不行。
从上面的例子我们可以看出,Lexical scoping
实际上是以链式
存在的。一个 scope
中可以开一个新的 scope
,而不同 scope
中是可以允许重名变量的。那么我们在某个 scope
中访问一个变量,究竟是基于什么规则来访问变量的呢。
在上面这个例子中我们可以看到,在 main 和 firstScope 中都定义了变量 a。我们在 firstScope 中 print,输出了 2 in firstScope 而在 main 中 print 则会输出 1 in mainScope 。
我们已经可以总结出规律了:近者优先。
如果你在某个 scope
中访问一个变量,它首先会看当前 scope
中是否已经定义该变量,如果已经定义,那么就使用该变量。如果当前 scope
没找到该变量,那么它就会在它的上一层 scope
中寻找,以此类推,直到最初的 scope
。如果所有 scope
链上都不存在该变量,则会提示 Error:Undefined name ‘name’。
Dart scope
中的变量是静态确定的
,如何理解呢?
我们可以看到,虽然在 main 的父 scope
中存在变量 a,且已经赋值,但是我们在 main 的 scope
中也定义了变量 a。因为是静态确定的,所以在 print 的时候会优先使用当前 scope
中定义的 a,而这时候 a 的定义在 print 之后,同样也会导致编译器错误:Local variable ‘a’ can’t be referenced before it is declared。
javascript作用域
javascript中的作用域
说来话长,容我慢慢道来:
首先明确几个概念,javascript中是没有块级作用域
的,只有函数作用域
和全局作用域
,看一下下面javascript的例子:
可以看到在if
的执行块中声明的a
,依然可以在执行块外面进行访问。并且绑定在了全局作用域上,这里的this
也就是window对象。所以this.a
可以输出inke。
Dart中是有块级作用域
的,看一下下面这个例子:
可见if
执行块中声明的变量是无法在外部进行访问的。
Dart中的if
,while
,for
等是有块级作用域
的,如上图所示,父级作用域中是无法访问子作用域中声明的变量的。
我们可知es6新出了两个东西,let
、const
。这两个声明变量的方式完全替代了var
。可是为什么es6会推出这两种声明变量的方式呢?
let
、const
声明的变量都在当前执行块中进行访问,这里就填补上了javascript没有块级作用域的缺陷。
为什么javascript最开始没有设计块级作用域呢?
翻了至少20篇文章之后,我找到了最接近真实的答案: 设计缺陷/bug
所以es6补充条案第一条就是let、const。并且基本上我们也不写var了。
我们继续探索,在javascript中只有两种作用域,函数作用域,全局作用域。全局作用域则不必多说。我们主要讲一下函数作用域。
Scopes
这里我要标记一下,下面有些内容我实在是没有找到实在的根据,有一些逻辑实属推断,如有错漏,欢迎指正。
javascript中的函数也可以在函数内部创建函数,那么scope
就会层层嵌套,又由于作用域的特性保留,子作用域可以访问父作用域的变量,所以生成了一条作用域链[[scopes]]
,看一下如下例子。
作用域链的具体表现形式我们debug中看一下:
可以看到function c
中的 Scopes
长度为3
,说明函数c的作用域链中有三个作用域,算自己本身的作用域的话就是四个。因为这个断点打在了b函数
的内部并且是 c函数
调用的位置。
在函数c内部打印的aa、bb、cc都会先从自身scope
寻找,然后向上寻找,直至寻找到顶级作用域global
。global举个例子:在浏览器中就是window对象。
这样就比较明白作用链是个什么东西了,而且比较耳熟,相信你们最开始了解原型链的时候也是这么背的,原理呢,其实差不是很多,下一节课就可以深入了解一下原型、继承等等。
Closure
这里有人可能有疑问,上面Scopes
中长度有三,最后一个Global可以理解,但是为什么函数a和函数b为(Closure)
?
这里先给出原因,因为在函数c中调用了函数a和函数b内部的参数,所以函数c中的作用域链中,有函数a和函数b的闭包
。
这里一定会有人有疑问:
我记得闭包应该是什么函数套函数,然后return一个函数的呀。
其他的先不讲,至少从我上面的例子中就已经推翻你这个结论了。
真实的概念:
「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
那我们印象里背的return
是怎么回事呢。如果不把这个闭包return
出来的话,外面就没法调用。。。只有return
出来后,就可以在外部上下文
进行函数内部变量的访问。
关于闭包还有一个谣言内存泄漏
,相信大多数人印象里闭包的缺点
一定就是有内存泄漏风险。
说这话的人根本不知道什么是内存泄露。内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量明明就是我们需要的变量(lives)
,凭什么说是内存泄露?
这个谣言是如何来的?
因为 IE
。IE
有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。
这是 IE 的问题,不是闭包的问题。参见司徒正美的这篇文章,传送门
现在IE已经渐渐退出历史舞台了,所以大家可以更新一下脑海中的观念了。
This
这里补充几个小知识点。
在javascript中面我们比较熟悉的东西叫做 this
。 this
是什么呢?你可能只记得this的指向问题,因为前端面试的时候总是要问对吧,但是我们问的并不是指向问题,而是this是什么?
this是函数当前执行时候的作用域,就像上面的作用域链中的例子中,函数c中的this指向的就是Global作用域
。当然,Global作用域也是scopes作用域链
中的一环。
我们可以通过apply
、call
、bind
来改变this
的指向
,这里所说的this指向就是指作用域链中的指向。
这里有人可能会问,如果我apply了一个函数d,怎么办呢?函数d并不在最开始的函数作用域链中,如果你用apply将this指向函数d的话,在作用域链
中将多一个Closure
。并且将this
指向过去。
这个时候this就是函数d
。
这里顺便提一下Dart中是没有this关键字的。
END
在本章节中我们学习了知识点:Dart函数、Lambda、词法作用域、Javascript作用域、作用域链、闭包、This等。
这个系列专栏目测快要结尾了,计划共8-9节。欢迎大家点赞转发。