函数定义
👉点击
【补充】:
注意区分什么是函数声明、什么是函数表示,不要混淆。
命定表达式
函数按照上面的方式定义后,只有test才是函数,test1不是一个函数,但是test1是test的name,可以通过test.name得到。
参数、返回值
👉点击
预编译
JavaScript执行的三部曲:
1、语法分析:先通篇扫描一下看有没有语法错误,但是不执行。
2、预编译
3、解释执行
【函数声明整体提升,变量的声明提升,变量的赋值不提升】
我们知道JavaScript是解释一行执行一行的,看下面的代码:
1 | test(); |
它是可以成功执行打印出666的。因为函数声明整体提升了,它会把函数的声明提升到逻辑的最前面。
再看下面的代码:
1 | console.log(a); |
报错,a未定义。
1 | console.log(a); |
可执行,但是打印结果为undefined。这说明了 a 是已经声明但是尚未赋值。这一现象是变量的声明提升。
但是,这两个口诀治标不治本,必须把预编译搞明白。
imply global 暗示全局变量:
任何变量,如果变量未经声明就赋值,此变量就为全局对象window所有。
1 | <script> |
再来一个栗子:
1 | function test() { |
这个执行结果是报错的。
var a = b= 10
是从右往左赋值的,先将10赋值给b,但是b没有声明所以它是一个全局变量,之后再把b赋值给a,a有声明变量,它是函数test里面的一个局部变量。外界是无法访问a的,所以报错。但是如果打印b是可以正确输出10的。
一切声明的全局变量,都归window所有,都是window的属性:
1 | <script> |
函数预编译
函数预编译发生在函数执行的前一刻。过程如下:
- 创建AO对象(Activation Object 执行期上下文,它的作用就是我们理解的作用域)
- 找形参和变量声明,将变量和形参名作为AO对象的属性名,值为undefined
- 将实参值和形参统一
- 找函数声明,将函数名也作为AO对象的属性名挂起来,将该声明的函数体作为值。
举个栗子👇
这段代码执行结果为?
栗子1👇
1 | function test(a, b) { |
栗子2👇
1 | function test(a, b) { |
全局预编译
和函数的预编译类似,同时它产生的叫做GO,GO就是window
- 创建GO对象(global object 全局上下文,它的作用就是我们理解的全局作用域)
- 找形参和变量声明,将形参名、变量名作为GO对象的属性名,值为undefined
- 找函数声明,将函数名也作为GO对象的属性名挂起来,将该声明的函数体作为值。
举个栗子👇
上面的栗子改动一下👇
来练练👇
1 | console.log(test); |
1 | console.log(bar()); |
1 | function bar() { |
作用域
每个JavaScript函数都是一个对象,对象中有些属性我们可以访问。也有些是不可以访问的,这种属性仅供JavaScript引擎存取,[[scope]]就是其中之一。
[[scope]]指的就是作用域,其中存储了运行期上下文的集合。
[[scope]]中所存储的执行期上下文对象的集合,这种集合呈链式链接,称作是作用域链
执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用同一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文就销毁。爽完了就抛弃,渣男!
1 | function test(){ |
举个栗子👇
这段代码首先是函数a的声明,a函数被定义的时候就产生了如下环节:
a的[[scope]]里面存的是scope chain即作用域链,这时候作用域链里只有一个全局执行上下文GO,紧接着执行代码var glob = 100
更新GO。紧接着执行函数a()。
a函数执行的时候就产生了它的执行期上下文AO,并把AO放到作用域链的最顶端,现前的GO就往后稍稍。
一旦我们再a里面查找一个变量的话,它会从a的作用域链的顶端——AO依次向下查找。
紧接着,由于a的执行产生了一个过程——b函数的定义,b作为函数也有自己的[[scope]],但是b刚出生时它的环境就是a给它的,所以b可以捡现成的[[scope]]。
即b刚刚被定义的时候,它保存的就是a的劳动成果。
重点在于b函数被执行的时候,它也产生了它的执行上下文AO,将其放在作用域链的最顶端,之前a的AO和GO就又往后稍稍了。
这时如果在b里面找一个变量的话,也是从作用域链的最顶端自顶向下查找。
b()执行完成之后,b自己的AO就被销毁了,b.[[[scope]]回到了b一开始被定义的时候;
接着b()执行完说明a()也执行完成了,a自己的AO也被销毁了,a.[[scope]]回到了a一开始定义的时候。
再举个栗子👇
1 | function a() { |
执行a(),得到作用域链如下:
在任何一个函数里面想要访问一个变量,找它的作用域链就完事了。想要访问变量一定是看在这个函数执行时产生的作用域链,而且是自顶向下访问。
假如我们要在b函数里面访问c函数里定义的cc变量,那么这时候我们是站在【b executing b.[[scoep]]】这个作用域链上的,而这个作用域链里面根本没有cAO,也就访问不到c函数里面定义的cc变量了。这就是为什么函数外部无法访问函数内部的变量的原因。
ps:上面这段代码如果a函数没有执行就没有【a executing 。。。】及其之后的作用域链。
闭包
一个栗子👇
1 | function a() { |
a执行完毕后销毁了aAO,讲道理,里面的aaa也应该销毁了,但是a函数把b返回了出来并且赋给了demo,导致demo保留了aAO,从而可以访问到aaa。最终结果是打印出123。
这就是闭包。
但凡函数内部的函数,被保存到了外面,一定生成闭包。
再来个详细一点的图文:
当内部函数被保存到外部时,将会生成闭包。
闭包的作用
实现公有变量
栗子:
1
2
3
4
5
6
7
8
9
10
11
12function add() {
var count = 0;
function demo() {
count++;
console.log(count);
}
return demo
}
//这样就把count这个变量公有化了
var counter = add();
setInterval(counter, 1000)
可以做缓存(存储结构)
可以实现封装,属性私有化
模块化开发,防止污染全局变量
【闭包的缺点】闭包会导致原有作用域链不释放,占用内存,造成内存泄漏。
立即执行函数
执行完立即销毁
两种格式:
- (function () { })()
- (function () { }())
1 | function test(){ |
而且打印test结果就是test函数的函数体。那么我这样写行不行呢?👇
1 | function test() { |
⚠很遗憾,不行!!!
这里记住:【只有表达式才能被执行符号()执行】
所以上面栗子中function test(){}
它是函数声明,不是表达式,不可以被执行;
而test()
中test
就算单独杵着它也是一个表达式,不是说非要a+b
之类的才叫表达式,单独的123;
、test
也是表达式了。
那么!函数表达式能不能被执行呢?
1 | var test = function () { |
都叫表达式了它就一定能被执行。
【奇技淫巧】
记住表达式能被立即执行则表达式的名字不存在,举个栗子👇
1 | var test = function () { |
在控制台操作如下:
这个test的名字还代表着这个函数。
但是,我们在函数表达式后边加一个括号,这个表达式就可以被立即执行了:
1 | var test = function () { |
控制台中打印test为undefined,它就不再代表函数了,function(){console.log(666)}
被永久的放弃了,这也符合立即执行函数的特点,用完就销毁。
1 | + function test() { |
函数声明后面加上括号是不可以被执行的,但是在函数声明前面加上一个加号,就会使得function test(){}
有一个转化为number类型的趋势,整个+ function(){}
就变成了一个表达式,,从而可以被括号执行。同样的减号也可以办到。但是乘号除号不可以。
以下的运算符都可以办到:
1 | 0 || function test() { |
被执行了之后,test就不再代表function (){}
了,变成了undefined。
以上的核心步骤就是【将函数声明变成表达式,就可以被立即执行了】
那么括号()也是运算符,将函数声明用括号包起来,就能将函数声明变成表达式了!因此可以被立即执行。这就是立即执行函数的原理。
好,来个迷惑又恶心的 栗子👇
1 | function test(a, b, c, d) { |
这个居然不报错!因为系统将其理解成了:
1 | function test(a, b, c, d) { |
虽然不报错,但也没执行test函数。
再来一个👇:
1 | var f = ( |
再来一个难的栗子👇:
1 | var x = 1; |
【一颗经典的栗子】👇:
1 | function test() { |
我们想要的结果是输出从0到9,但是结果是输出了十个10。为啥呢?
在for循环的过程中,把函数体function(){console.log(i)}
赋给了arr[0]、arr[1]、arr[2]。。。。。arr[9],注意不是把function(){console.log(0)}
赋给了arr[0]、把function(){console.log(1)}
赋给arr[1]。。。把function(){console.log(9)}
赋给arr[9]。这点要搞清楚。所以循环结束,是把十个相同的函数体赋给了数组arr,而在执行arr里的每一个元素即每一个函数体的时候,才会给函数体里的 i 进行赋值,而赋值的时候可以看到 i 的值为10!所以最终打印除了十个10。
{建议手写GO、AO作用域链,可以更理解清晰)
解决方案:
方法一:利用立即执行函数
1 | function test() { |
方案二:另一种立即执行函数
1 | function test() { |
以上栗子的实际应用如下👇
1 | // 给每个li添加点击事件,点击时打印该li的顺序 |
with
with 可以改变作用域链
1 | var obj = { |
with(obj){…}括号里填入对象obj,会把这个对象obj当作with圈定的代码体的作用域链的最顶端,即把obj当作最顶端的AO(执行上下文),所以打印出来的name就是obj里的name。
💡应用场景
之前说过命名空间
1 | var org = { |
⚠使用with会降低效率,所以
ES5严格模式禁止使用它。