函数
函数的概念
function
-
是js内的一种数据类型,并且是一个引用数据类型(复杂数据类型)
-
函数在js中被叫做“一等公民”。
-
什么是函数(不是官方说法)
- 函数就是一个“盒子”,能装一段代码
- 当我们需要让这段跑起来的时候,只需要告诉“盒子”
函数的两个阶段(重要)
函数的定义阶段
-
方式1:声明式函数(函数声明式)
- 语法:function 名称(){}
- function 定义函数的关键字
- 名字 函数名(变量)遵循变量的命名规则和规范
- () 必须写,书写参数的位置
- {}放的是代码段,也就是我们需要放在“盒子”里面的代码 — 函数体
注意:函数的定义阶段,{}里面的代码不会执行
-
方式2:赋值式的函数(函数表达式)
- var 名字 = function(){}
函数调用阶段
- 语法:函数名()
- 意义:找到函数名对应的那个函数盒子,把盒子里面的代码执行一遍
注意:两种函数定义方式不同,但调用方式是一样的。
函数的参数(重点!!!)
形参和实参
-
第一种:形参(形式参数)
- 书写在函数定义阶段的小括号内
- 就是一个只能在函数内部使用的变量
- 可以书写多个,中间以逗号分隔
- 形参的值由实参决定。
-
第二种:实参(实际参数)
- 书写在函数的调用阶段的小括号内
- 按照从左往右的顺序依次对每个形参进行赋值。
函数的注意
函数调用的时候
- 函数声明式可以先调用,可以后调用
- 函数表达式只能后调用,先调用会报错
// fn(); // 456
// console.log(fn2); // is not a function
function fn (){
console.log(456);
}
var fn2 = function(){
console.log(123);
}
参数的数量
-
建议形参和实参数量一致
-
形参多
- 前面的按照从左到右的顺序依次赋值
- 多出来的形参,没有实参进行赋值
- 在函数内使用的时候,就是undefined
-
实参多
- 前面的按照从左到右的顺序依次赋值
- 多出来的实参,在函数内没有形参接受
- 在函数内不能直接使用,可以通过 arguments 间接使用
arguments 实参列表
-
函数内部天生自带的一个变量
-
是一个“盒子”,内部存放不是代码,而是你传递的所有实参
- 我们调用函数的时候传递了多少个实参,这个盒子里就有多少个数据
- 叫做实参的集合,arguments 内的所有数据都是按照“序号排列”
- 注意:序号是从 0 开始,依次 +1
- 索引/下标
-
访问 arguments内的数据
- 利用索引(下标)来进行访问
- 语法:arguments[索引]
- 如果 arguments 内有对应的索引,那就拿到对应位置的数据
- 如果没有对应的索引,那就是undefined
-
arguments 的 length 可以让我门知道 传递了多少个实参
-
遍历 arguments
- 遍历:从头到尾,不重不漏的访问每一个数据(循环而已,只不过有个酷炫名字)
函数的 return(重点)
- 返回结果值(给出返回值)
- 中断函数的执行
其实就是把函数中我们想要返回给外面的结果,丢出去,只不过有个好听名字叫做返回值
在函数体内, return 关键字用来给函数一个返回值,在哪里调用这个返回值就会给到哪里
function sum(){
var n = 0;
return n;
for(var i = 0; i <= arguments.length - 1; i++){
n += arguments[i];
console.log(i);
}
}
sum(1,2,3,10,5,7)
var a = parseInt(10.1234213)
console.log(a);
var b = sum(1,2,3,4,5,6,7,8,9);
console.log(b);
函数进阶(重点)
作用域
js三座大山 1.作用域 2.异步 3.原型
什么是作用域
- 就是一个变量可以生效的范围
- 变量不是在任何地方都可以使用的,这个变量的使用范围就是作用域
浅谈 js 编译原理
javascript通常被称为“动态”或“解释执行”语言,但事实上它是一门编译语言。
但与传统的编译语言不同,他不是提编译的,编译结果也不能在分布式系统中进行移植。
但是js引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能 比预想要更加复杂
在传统编译语言的流程中,程序中的一段缘吗在执行之前会经历三个步骤,统称为“编译”
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码被称为词法单元(token)。
- 作用:将代码分解成代码块(词法单元)
- 分词和词法分析的区别:
分词和词法分析之间的区别非常微妙,主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析。- 在生成词法单元时,如果生成的词法单元是未声明的(词法单元生成器调用无状态的解析规则),解析的过程被称为分词。
- 如果生成的词法单元是已经声明的(词法单元生成器调用有状态的解析规则),解析的过程被称为词法分析
var a = 1;// 此处是分词,词法单元生成器将其解析为 var a = 1 ;五个词法单元
a = 2; // 此处就是词法分析,因为a已经声明了,词法单元生成器将调用有状态的解析规则
-
解析/语法分析(Parsing)
这个过程是将词法单元转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree AST)
作用:将词法单元流转换为有层级关系的抽象语法树(Abstract Syntax Tree AST) -
代码生成
作用:将AST转换为可执行代码的过程被称为代码生成
具体点说就是某种方法可以将 var a = 2; 的AST转化为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存等),并将一个值储存在a中。
理解作用域
相关概念
- 引擎:负责整个js程序的编译及执行过程
- 编译器:负责词法分析、语法分析及代码生成
- 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
// 分解 var a = 2;
/*
- 首先编译器会将这段程序分解成词法单元
- 然后将词法单元解析成一个树结构
- 之后当编译器来时进行代码生成时,会做两件事
1. 遇到var a,编译器会询问作用域是否已经有一个改名称的变量存在于同一作用域中
- 如果有,编译器就会忽略该声明,继续进行编译
- 如果没有,那么他会要求作用域在当前作用域中声明一个新的变量,并命名为 a
2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2这个赋值操作。
- 引擎运行时会首先询问作用域,在当前的作用域中是否存在一个叫做a的变量
- 如果有引擎会使用这个变量,如果没有引擎会继续查找该变量(再往后会干嘛,后面说)
最终引擎找到a变量,就会将2赋值给他,如果没找到引擎就会抛出一个异常
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对他赋值。
*/
编译器中的两个概念
- LHS和RHS:是变量的两种查询方式,可以简单理解为负值操作的左侧还是右侧(注意:有些赋值操作是隐式的,比如形参)
讲的准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图 找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋 值操作的右侧”,更准确地说是“非左侧”。
- LHS:因为需要为变量赋值,所以要查询到变量的容器本身
- RHS:赋值操作的右侧变量的查询,此时只用关心右侧变量的值,所以需要查询变量在内存中的值
// 其中对a的引用是一个RHS引用,因为这里 a 并没有赋予任何值。相应地,需要查询并取得 a 的值,这样才能将值传递给打印
var a = 1;
console.log(a);
// 这里对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为 = 2这个赋值操作找到一个目标
a = 2;
function foo(a){
var b = a;
return a + b;
}
var c = foo(2)
// LHS 3 c=...\a = 2(隐式变量赋值) \b=....
// RHS 4 foo(2) \ =a \ a...\b...
作用域的分类
- 全局作用域
- 打开一个页面就是一个全局作用域
- (私有作用域)函数作用域
- **只有函数生成私有作用域)
作用域的上下级关系
- 你在那个作用域中书写的函数,就是那个作用域的子级作用域
function fn(){
// fn函数内,就是一个私有作用域
// fn私有作用域
// fn的父作用域是全局作用域
// 子级作用域是fun私有作用域
function fun(){
// fun函数空间内,也是一个私有作用域
// 父级 fn私有作用域
// fun私有作用域
}
}
作用域的机制
作用域就是关注变量的,他给我们提供了三种机制
- 变量的定义机制
- 变量的赋值机制
- 变量的访问机制
变量的定义机制
你定义在那个作用域下的变量
就是该作用域下的私有变量
只能在该作用域下及其后代作用域中使用
var n1 = 100;
function fn(){
var n2 = 200;
function fun(){
var n3 = 300;
console.log(n1);
console.log(n2);
console.log(n3);
}
fun()
}
fn()
console.log(n1);
console.log(n2); // 报错
console.log(n3); // 报错
变量的访问机制
当你需要获取一个变量的值的时候
首先在自己的作用域中查找,如果有则直接使用,停止访问
如果没有,一级一级往上查找
直到 window 上都没有,那就报错
function fn(){
// var a = 30;
function fun(){
var a = 20;
}
fun()
function fun2(){
console.log(a); // 报错
function fun3(){
var a = 40;
}
fun3()
}
fun2()
}
fn()
变量的赋值机制
当你给一个变量赋值的时候
如果自己的作用域中有该变量,那么就给自己作用域中的变量赋值
如果没有,那就会给父级的变量赋值
如果父级还没有,继续找
直到window,如果window都没有
那就会把这个变量定义为全局变量,在进行赋值
function fn(){
function fun(){
a = 30;
}
fun();
console.log(a); // 30
}
fn()
console.log(a); // 30
console.log(window);
提升(预解析)
- 在所有代码执行之前,对代码进行通读并解释
解析了var关键字
会把var关键字声明进行提前说明,但是不进行赋值
console.log(num);//undefined
var num = 100;
console.log(num); // 100
/*
打开页面
预解析
=》var num
-》告诉浏览器我定义了一个叫做num的变量 但没有赋值(提升只会将定义变量提升(var num)= 100这个操作不会进行提升)
执行代码
=第一行代码,在控制台打印num变量
- 因为预解析的时候,已经声明过num变量,但是赋值操作并没有被提升
- num变量存在
- 打印出来 undefined
= 第二行代码
- 给已经声明的num赋值为100
= 第三行代码
- 在控制台打印num
- 由于第二行,num已经被赋值为100
- 所以,打印出来的是100;
*/
解析了 声明式 函数
- 在所有代码执行之前,会把函数名进行声明提前,并赋值为一个函数
/*
打开页面
预解析
= function fn(){}
- 告诉浏览器,我定义了一个fn变量,并且这个fn内保存的是一个函数
代码执行
= 第一行代码 fn()
- 拿到fn变量存储的值,当作一个函数来使用
- 因为预解析阶段 fn保存的是一个函数
- 调用没有问题
*/
重名问题
- 当你使用var定义的变量和声明式函数重名的时候,以函数为准
- 只限于在预解析阶段,以函数为准
num(); // 打印
var num = 100;
console.log(num); // 100
function num(){
console.log("我是num函数");
}
console.log(num); // 100
递归函数
- 函数一种应用方式
- 递:一层一层的进去
- 归:一层一层的回来
- 把一件事情分成若干个事情来做
- 递归就是一个自己调用自己的手段
- 递归函数:一个函数内部,调用自己,循环往复
- 递归终点
- 当达到设置的终点的时候
- 在归回来,归使用return
- 当不设置终点的时候
- 就会陷入死循环
- 注意:写递归先写终点(先写停 return)
注意:递归慎用,能用循环解决的事情,尽量别用递归
function jc(n){
if(n == 1) return 1;
return n * jc(n - 1);
}
var res = jc(5)
/*
5 * 4 * 3 * 2 * 1
4 * jc(3)
3 * jc(2)
2 * jc(1)
1
5 * 4 * 3 * 2 * 1;
*/
console.log(res); // 120