一、函数
1.1 函数的定义和调用
• 函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的 函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执 行,要不执行就都不执行。
• 函数的作用就是封装一段代码,将来可以重复使用。
• 使用typeof检查一个函数对象时,会返回function
1.1.1 函数声明
• 函数声明又叫函数定义,函数必须先定义然后才能使用。
• 如果没有定义函数直接使用,会出现一个引用错误。
• 函数声明语法:
function 函数名(参数){
封装的结构体;
}
特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行。
1.1.2 函数调用
• 调用方法:函数名();
• 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行。
• 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关。
• 函数可以一次定义,多次执行。
//定义函数,函数就是一组语句的集合
function haha(){
console.log(1);
console.log(2);
console.log(3);
console.log(4);
}
//调用函数
haha();
函数的意义1:
在出现大量程序相同的时候,可以封装为一个function,这样只用调用一次,就能执行很多语句。
1.2 函数的参数
为什么要设置参数?
为了增强函数的功能性和函数的可拓展性,便于交互。
• 我们希望函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化。
• 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化。
• 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数 执行结果根据参数不同,结果也不同。
• 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔。
• 函数的参数根据书写位置不同,名称也不同:
• 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传 递过来的数据。简称形参。
• 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参。
• 实际参与运算的变量。形参为他占位置,真实参与运算的变量。
• 函数执行过程,伴随着传参的过程:
函数的参数优点
• 不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么 功能,没必要知道内部的结构什么。
• 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要 传递什么类型的数据,实现什么功能。
参数可以有无数个,用逗号隔开。
//有多少形式参数都可以,都罗列出来
function fun(a,b){
console.log(a + b);
}
fun(3,5); //输出8
fun(8,11); //输出19
函数的意义2:
我们在调用一个函数的时候,不用关心函数内部的实现细节,甚至这个函数是你上网抄的,可以运用。所以这个东西,给我们团队开发带来了好处。
1.3 函数的返回值
• 函数能够通过参数接收数据,也能够将函数执行结果返回一个值。
• 利用函数内部的一个 return 的关键字设置函数的返回值。
• 作用 ①:函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行。
• 作用 ②:可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式, 函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续 可以参与程序,表达式的值就是 return 后面的数据。
函数的返回值应用
• 函数如果有返回值,执行结果可以当成普通数据参与程序。
• 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际 参数。
• 注意:
如果函数没有设置 return 语句 ,那么函数有默认的返回值 undefined;
如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined。
如果函数使用 return语句,那么跟再return后面的值,就成了函数的返回值。
推荐的做法是要么让函数始终都返回一个值,要么不要有返回值。
function sum(a,b){
return a + b; //现在这个函数的返回值就是a+b的和
}
console.log(sum(3,8)); //sum没有输出功能,就要用console.log输出
//sum(3,8)实际上就成为了一个表达式,需要计算
//计算后就是11,console.log(11);
函数只能有唯一的return,但有if语句时除外。
程序遇见了return,将立即返回结果,返回调用它的地方,而不执行函数内的剩余的语句。
function fun(){
console.log(1);
console.log(2);
return; //返回一个空值
console.log(3); //这行语句不执行,因为函数已经return了
}
fun();
函数的意义3:
模块化编程,让复杂的逻辑变得简单。
1.4 函数常见的声明方式
1.4.1 函数声明方式
function add(num1,num2){
return num1+num2;
}
1.4.2 函数表达式声明方式
var add= function(num1,num2){
return num1+num2;
};
1.4.3 使用Function构造函数
var add = new Function('num1','num2','return num1+num2');
不推荐使用, 主要用于面向对象时理解"函数就是对象, 函数名就是指针"这一概念。
1.5 函数的数据类型
• 函数是一种单独的数据类型 Function。
• 由于函数是一种数据类型,可以参与其他程序。
• 例如,可以把函数作为另一个函数的参数,在另一个函数中调用。
• 或者,可以把函数可以作为返回值从函数内部返回。
// 定义一个函数
function fun() {
console.log(1);
}
// 定义一个函数表达式
var foo = function () {
console.log(2);
};
// 检测函数的数据类型
console.log(typeof (fun())); // undefined
console.log(typeof (fun)); // function
console.log(typeof (foo)); // function
函数是一个引用类型
基本数据类型:number、string、boolean、undefined、null
引用数据类型也有很多种:object、function、array、RegExp、Math、Date。
归根结底,它还是属于object
基本类型保存值,引用类型保存地址
var a = 6 ; 那么这个a变量里面存储的是6这个数值;
而var a = function(){}; 那么这个a标签里面存储的是function的内存地址。
a、b指向的是内存中的同一个地址,所以a就是b,b就是a。
//定义了一个变量a,引用了一个funciton
//这个a变量存储的是这个匿名函数的内存地址
var a = function(){
alert("我是一个函数,我执行了");
}
//就是把匿名函数的地址也给了b
var b = a;
//给b添加一个属性
b.haha = 1;
//输出a的haha属性,你会发现a也有这个属性了:
console.log(a.haha);
//b的haha属性和a的同步更改的,因为都是指向的同一个对象
b.haha++;
b.haha++;
b.haha++;
console.log(a.haha);
1.6 arguments 对象
• JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有的实参。arguments 是一个伪数组(不是数组),因此及可以进行遍历。
• 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中。
• arguments对象还有一个名叫callee的属性, 该属性是一个指针, 指向拥有这个arguments对象的函数;
// 定义一个函数
function sum(a,b) {
return a + b;
}
// 调用函数的时候,实参的个数可以与形参不同
console.log(sum(1,2)); // 3
console.log(sum(1)); // NaN
console.log(sum(1,2,3,4)); // 3
// 函数内部有一个 arguments 对象,会接收所有的实参
function fun() {
console.log(arguments);
console.log(arguments.length);
// 使用数组的遍历方法可以获取每一项实参
for (var i = 0 ; i <= arguments.length - 1 ; i++) {
console.log(arguments[i]);
}
}
// 调用函数
fun(1,2,3,4,5,6,7);
1.7 函数递归
• 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象。
• 递归的次数太多容易出现错误:超出计算机的计算最大能力。
• 更多时候,使用递归去解决一些数学中的现象。
• 例如可以输出斐波那契数列的某一项的值。
斐波那契数列就是经典的递归算法: 1、1、2、3、5、8、13、21、34、55、89、144、233……
function fun(n) {
if (n === 1 || n === 2) {
return 1;
} else {
return fun(n - 1) + fun(n - 2);
}
}
// 调用函数
console.log(fun(1)); // 1
console.log(fun(2)); // 3
console.log(fun(3)); // 6
1.8 匿名函数
没有命名的函数:function () {};
作用:
1)、用在绑定事件的时候
document.onclick = function () {
alert(1);
};
2)、定时器
setInterval(function () {
console.log('每秒爱你1遍!');
},1000);
3)、立即执行函数(闭包) 函数定义完,立即被调用,这种函数叫做立即执行函数;立即执行函数往往只会执行一次
(function(num1, num2){
console.log("mum1 = "+ num1);
console.log("num2 = "+ num1);
console.log("num2 = "+ num2);
})(100, 101);
1.9 作用域
• 作用域:变量可以起作用的范围。
• 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量 ,函数就是变量定义的作用域。
• 任何一对花括号 {} 中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
• 在es5之前没有块级作用域的的概念,只有函数作用域,现阶段可以认为 JavaScript 没有块级作用域.
全局变量和局部变量
• 局部变量:定义在函数内部的变量,只能在函数作用域内部被访问到,在外面没有定义的。
• 全局变量:从广义上来说,也是一种局部变量,定义在全局的变量,作用域范围是全局, 在整个 js 程序任意位置都能够被访问到。
• 变量退出作用域之后会销毁,全局变量关闭网页或浏览器才会销毁。
// 全局、局部、块级
// 全局变量
var dog = '小花';
// 局部变量
function sum() {
// 局部变量
var sex = '男';
console.log(sex); // 男
console.log(dog); // 小花
}
sum();
console.log(sex); // sex is not defined
// 块级
{
var str = '小撩';
var age = 19;
}
console.log(str, age);
作用域链
• 只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
• 将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。
var a = 0;
// 定义函数
function fun() {
var a = 1;
console.log(a); // 1
}
// 执行函数
fun();
// 函数外部调用 a
console.log(a); // 0
// 函数的参数也是局部变量
function fun(a) {
a = 2;
console.log(a); // 2
}
// 调用函数
fun(1);
console.log(a); // 报错 a is not defined
遮蔽效应
• 程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名 字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变 量定义会按照顺序从本层往外依次查找,直到找到第一个变量定义。整个过程中会发生内 层变量遮蔽外层变量的效果,叫做“遮蔽效应”。
// 函数也有自己的作用域
var a = 1;
function outer() {
var a = 2;
function inner() {
var a = 3;
console.log(a); // 3
}
// 函数内部调用子函数才能成功
inner();
console.log(a); // 2
}
// 调用函数
outer();
// inner(); // 不能调用 inner() 子函数
console.log(a); // 1
参数也是局部变量
• 函数的参数本质是一个变量,也有自己的作用域,函数的参数也是属于函数自己内部的局 部变量,只能在函数内部被使用,在函数外面没有定义。
不写 var 关键字的影响 • 在函数内部想要定义新的变量,如果不加关键字 var,相当于定义的全局变量。如果全局也 有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量。
• 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局。
1.10 不写 var 关键字的影响
• 在函数内部想要定义新的变量,如果不加关键字 var,相当于定义的全局变量。如果全局也 有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量。
• 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局。
// 全局作用域
var a = 1;
console.log(a); // 1
// 创建函数
function outer() {
a = 2;
// 内部函数
function inner() {
var a = 3;
console.log(a); // 3
}
inner();
console.log(a); // 2
}
// 调用
outer();
console.log(a); // 2 (污染全局造成的)
1.11 函数的作用域
• 函数也有自己的作用域,定义在哪个作用域内部,只能在这个作用域范围内被访问,出了 作用域不能被访问的。
• 函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写。
1.12 预解析
• JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器执行JavaScript 代码的时候,分为两个过程:预解析过程和代码执行过程。
• 预解析过程:
1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
3. 先提升 var,再提升 function。
• JavaScript 的执行过程:在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码。
变量声明提升
• 在预解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在 将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程。
• 提升过程中,只提升声明过程,不提升变量赋值,相当于变量定义未赋值,变量内存储 undefined 值。
• 因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined 值。
函数声明提升
• 在预解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在 将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程。
• 在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义 成功,后续就可以直接调用函数。
• 因此,在 js 中会出现一种现象,在前面调用后定义的函数,不会报错,而且能正常执 行函数内部的代码。
// 模拟提升
/*
var a; //相当于存了一个undefined的值
var fun;
*/
// 调用一个变量
console.log(a); // undefined
console.log(fun); // ƒ fun() {console.log(2);}
var a = 1;
function fun() {
console.log(2);
}
// 后定义变量
var a = 1;
a = 1;
var fun = "haha";
fun = "haha";
// 先调用函数
fun();
// 定义函数
function fun() {
console.log(2);
}
// 调用
fun();
函数表达式的提升
• 在预解析过程中,函数表达式进行的是变量声明提升,而不是函数声明提升。提升后变 量内部存的是一个 undefined。在前面进行函数方法调用,数据类型会提示错误。
• 建议:定义函数时,最好使用 function 关键字定义方式,这样函数声明提升可以永远生 效。
console.log(foo); // undefined
foo();
// 函数表达式进行的是变量声明提升,而不是函数声明提升
var foo = function () {
console.log(3);
};
提升顺序
• 预解析过程中,先提升 var 变量声明,再提升 function 函数声明。
• 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那 么在后续代码中出现调用标识符时,内部是函数的定义过程,而不是 undefined。
• 如果调用标识符的过程在源代码函数和变量定义后面,相当于函数名覆盖了一次变量名, 结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用 的就是变量存的新值。
• 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖。
1.13 IIFE 自调用函数(立即执行函数)
• IIFE:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数,表示函数在定义时就立即调用。
• 函数调用方式:函数名或函数表达式的变量名后面加 () 运算符。
• 函数名定义的形式不能实现立即执行自调用,函数使用函数表达式形式可以实现立即执 行,原因是因为函数表达式定义过程中,将一个函数矮化成了一个表达式,后面加()运 算符就可以立即执行。
• 启发:如果想实现 IIFE,可以想办法将函数矮化成表达式。
• 函数矮化成表达式,就可以实现自调用。
• 函数矮化成表达式的方法,可以让函数参与一些运算,也就是说给函数前面加一些运算 符。
数学运算符:+ - () 逻辑运算符:!非运算
• IIFE 结构可以关住函数的作用域,在结构外面是不能调用函数的。
• IIFE 最常用的是 () 运算符,而且函数可以不写函数名,使用匿名函数。
// 关键字定义的方式,不能立即执行
// function fun() {
// console.log(1);
// }();
// 函数表达式方式,可以在定义时被立即执行
var foo = function fun() {
console.log(2);
}();
// 通过在函数前面添加操作符,可以将函数矮化成表达式
+ function fun() {
console.log(1);
}();
- function fun() {
console.log(1);
}();
(function fun() {
console.log(1);
})();
!function fun() {
console.log(1);
}();
// *function fun() {
// console.log(1);
// }();
// IIFE 关住了函数的作用域,在外面是调用不了函数的
// fun();
// 常用的 IIFE 结构
(function (a) {
console.log(a);
})(4);
// console.log(a);
二、闭包
function outer(){
var a = 333;
function inner(){
console.log(a);
}
return inner;
}
var inn = outer();
inn(); //弹出333
inner()这个函数不能在outer外面调用,因为outer外面没有inner的定义:
function outer(){
var a = 888;
function inner(){
console.log(a);
}
}
//在全局调用inner但是全局没有inner的定义,所以报错
inner();
但是我们现在就想在全局作用域下,运行outer内部的inner,此时我们必须想一些奇奇怪怪的方法。 有一个简单可行的办法,就是让outer自己return掉inner:
function outer(){
var a = 333;
function inner(){
console.log(a);
}
return inner; //outer返回了inner的引用
}
var inn = outer(); //inn就是inner函数了
inn(); //执行inn,全局作用域下没有a的定义
//但是函数闭包,能够把定义函数的时候的作用域一起记忆住
//能够输出333
这就说明了,inner函数能够持久保存自己定义时的所处环境,并且即使自己在其他的环境被调用的时候,依然可以访问自己定义时所处环境的值。 一个函数可以把它自己内部的语句,和自己声明时所处的作用域一起封装成了一个密闭环境,我们称为“闭包”
每个函数都是闭包,每个函数天生都能够记忆自己定义时所处的作用域环境。但是,我们必须将这个函数,挪到别的作用域,才能更好的观察闭包。这样才能实验它有没有把作用域给“记住”。
我们发现,把一个函数从它定义的那个作用域,挪走,运行。嘿,这个函数居然能够记忆住定义时的那个作用域。不管函数走到哪里,定义时的作用域就带到了哪里。这就是闭包。
闭包在工作中是一个用来防止产生隐患的事情,而不是加以利用的性质。
闭包的性质
每次重新引用函数的时候,闭包是全新的。
function outer(){
var count = 0;
function inner(){
count++;
console.log(count);
}
return inner;
}
var inn1 = outer();
var inn2 = outer();
inn1(); //1
inn1(); //2
inn1(); //3
inn1(); //4
inn2(); //1
inn2(); //2
inn1(); //5
无论它在何处被调用,它总是能访问它定义时所处作用域中的全部变量