前端学习Dart|第六节

前言

这里是前端学习Dart第六节,本节知识点:Dart函数Lambda词法作用域Javascript作用域作用域链闭包This等。

视频地址:传送门

函数

函数的概念是可读,可维护和可重用代码的构建块。函数是一组用于执行特定任务的语句。函数将程序组织成逻辑代码块。一旦定义,就可以调用函数来访问代码。这使得代码可以重用。此外,函数可以轻松读取和维护程序的代码。

函数声明告诉编译器函数的名称,返回类型和参数。函数定义提供函数的实际主体。

看一下基本函数的声明:

image.png

依然是先声明后调用的方式,如果先调用是会报错的。

Error: Method not found: ‘action’.

函数的返回值

与js函数一致,函数是可以返回任意类型数据的,但是Dart中函数是具有返回值类型检查的可以开启。比如下方方式

image.png

无返回值函数可以用void类型检查。

image.png

函数的参数

参数是一种将值传递给函数的机制。参数构成函数签名的一部分。参数值在调用期间传递给函数。除非明确指定,否则传递给函数的值的数量必须与定义的参数数量相匹配。

函数的参数声明有几种方式:

  1. 必须传的参数

在函数调用期间必须将值传递给所需的参数。

image.png

可以看到上述函数action声明了两个传入参数(形参),但是在函数调用的时候只传入了一个参数,因为这样声明函数参数是必须传的参数,所以抛了一个错误。

Error: Too few positional arguments: 2 required, 1 given

  1. 参数位置可选

要指定可选的位置参数,请使用square 方括号 [] 括号。修改一下上面那个例子:

image.png

可以看到age参数被括了起来之后,调用函数的时候就不报错了,可选位置参数未传入的话默认值是null

多个可选位置参数是这样声明的

image.png

这里有个注意点:Dart函数中的所有必须参数必须在可选位置参数之前声明。一定要注意。

可选位置参数可以赋予默认值:

image.png

默认值必须是编译时常量 const , final则不行,因为final是运行时常量

  1. 可选命名参数

与位置参数不同,必须在传递值时指定参数名称。Curly brace 花括号 {} 可用于指定可选的命名参数。

首先这样的声明方式依然是可选的,可传可不传。

image.png

但是在传入这个可选命名参数的时候必须使用声明的变量名称

image.png

可选命名参数也支持声明时赋予默认值

image.png

注意事项:可选命名参数与位置可选参数只能出现一种,不可以两种同时出现。

  1. 函数作为参数

Dart中支持函数作为参数传入函数。

image.png

箭头函数

如果函数只包含一个表达式,那么可以使用箭头方式使用。

=> expr; 语法是 { return expr; } 的缩写

image.png

这里有一个注意事项:在箭头与分号之间只能出现表达式,而不能是语句,例如:不可以在那里放置if语句,但是可以使用条件表达式。

可以看到箭头函数是具有返回值的,当然你也可以设置void函数返回类型,这样的话就屏蔽掉了返回值。

image.png

箭头函数又称之为Lambda函数,那么什么是Lambda呢。

Lambda

可知函数的概念为代码块

首先有一个问题,就是要将一个函数赋值给一个变量要怎么赋值呢,在java前面的版本函数值不可以赋值给一个变量的,后来可以了,但是是类似这个样子的:

image.png

当然,这个并不是一个很简洁的写法。所以,为了使这个赋值操作更加优雅, 我们可以移除一些没用的声明,就开始了如下演变

image.png

这样,我们就成功的非常优雅的把一块代码赋给了一个变量。而这块代码,或者说这个被赋给一个变量的函数,就是一个Lambda表达式

诶,如果上述最后转换成的样子叫做表达式的话,右侧的匿名函数就是Lambda

匿名函数:

大多数函数都是命名的,例如main()print()等,我们还可以创建一个无名函数,称为匿名函数,有时候也称为labmbda或者闭包。看一下下面的例子

image.png

作用域

Dart的函数作用域就是词法作用域 Lexical scoping。为静态作用域

无论函数在哪里调用,也无论它如何被调用,他的词法作用域都只有函数被声明时所处的位置决定。

所谓词法作用域就是这样的一个静态的作用域模型,由全局作用域函数创建的作用域和块作用域等组成。

一个 Lexical scoping 内部 是能够访问到 外部 Lexical scoping 中定义的变量的。

image.png

这里出现了未定义该变量的错误警告,可以看出 printName 中定义的变量,对于 main 函数中的变量是不可见的。Dart 和 JavaScript 一样具有链式作用域,也就是说,子作用域可以访问父(甚至是祖先)作用域中的变量,而反过来不行。

从上面的例子我们可以看出,Lexical scoping 实际上是以链式存在的。一个 scope 中可以开一个新的 scope,而不同 scope 中是可以允许重名变量的。那么我们在某个 scope 中访问一个变量,究竟是基于什么规则来访问变量的呢。

image.png

在上面这个例子中我们可以看到,在 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 中的变量是静态确定的,如何理解呢?

image.png

我们可以看到,虽然在 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的例子:

image.png

可以看到在if的执行块中声明的a,依然可以在执行块外面进行访问。并且绑定在了全局作用域上,这里的this也就是window对象。所以this.a可以输出inke。

Dart中是有块级作用域的,看一下下面这个例子:

image.png

可见if执行块中声明的变量是无法在外部进行访问的。

image.png

Dart中的if,while,for等是有块级作用域的,如上图所示,父级作用域中是无法访问子作用域中声明的变量的。

我们可知es6新出了两个东西,letconst。这两个声明变量的方式完全替代了var。可是为什么es6会推出这两种声明变量的方式呢?

letconst声明的变量都在当前执行块中进行访问,这里就填补上了javascript没有块级作用域的缺陷。

为什么javascript最开始没有设计块级作用域呢?

翻了至少20篇文章之后,我找到了最接近真实的答案: 设计缺陷/bug

所以es6补充条案第一条就是let、const。并且基本上我们也不写var了。

我们继续探索,在javascript中只有两种作用域,函数作用域,全局作用域。全局作用域则不必多说。我们主要讲一下函数作用域。

Scopes

这里我要标记一下,下面有些内容我实在是没有找到实在的根据,有一些逻辑实属推断,如有错漏,欢迎指正。

javascript中的函数也可以在函数内部创建函数,那么scope就会层层嵌套,又由于作用域的特性保留,子作用域可以访问父作用域的变量,所以生成了一条作用域链[[scopes]],看一下如下例子。

image.png

作用域链的具体表现形式我们debug中看一下:

image.png

可以看到function c 中的 Scopes长度为3,说明函数c的作用域链中有三个作用域,算自己本身的作用域的话就是四个。因为这个断点打在了b函数的内部并且是 c函数调用的位置。

作用域链.png

在函数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),凭什么说是内存泄露?

这个谣言是如何来的?

因为 IEIE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。

这是 IE 的问题,不是闭包的问题。参见司徒正美的这篇文章,传送门

现在IE已经渐渐退出历史舞台了,所以大家可以更新一下脑海中的观念了。

This

这里补充几个小知识点。

在javascript中面我们比较熟悉的东西叫做 thisthis是什么呢?你可能只记得this的指向问题,因为前端面试的时候总是要问对吧,但是我们问的并不是指向问题,而是this是什么?

this是函数当前执行时候的作用域,就像上面的作用域链中的例子中,函数c中的this指向的就是Global作用域。当然,Global作用域也是scopes作用域链中的一环。

我们可以通过applycallbind来改变this指向,这里所说的this指向就是指作用域链中的指向。

这里有人可能会问,如果我apply了一个函数d,怎么办呢?函数d并不在最开始的函数作用域链中,如果你用apply将this指向函数d的话,在作用域链中将多一个Closure。并且将this指向过去。

image.png

这个时候this就是函数d

这里顺便提一下Dart中是没有this关键字的。

END

在本章节中我们学习了知识点:Dart函数、Lambda、词法作用域、Javascript作用域、作用域链、闭包、This等。

这个系列专栏目测快要结尾了,计划共8-9节。欢迎大家点赞转发。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享