1:初识函数
函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。
JavaScript 使用关键字 function 定义函数。函数可以通过声明定义,也可以是一个表达式。
2:函数声明
// 函数的使用 分为两步:声明函数和调用函数
//声明函数
// function 函数名() {
// //函数体
// }
// 调用函数
// 1:function 声明函数的关键字 全部小写
// 2: 函数是做某件事函数名一般是动词
// 3:函数不调用自己不执行
function getSum() {
var sum = 0;
for (var i = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
}
// 调用函数
getSum();
//函数的作用:功能的封装,直接调用,代码复用率提高
3:函数的参数
// 利用函数的参数实现函数重复不同的代码
// function 函数名(形参1;形参2...){// 在声明函数的小括号里面的是形参
// // 函数体
// }
// 函数调用
// 函数名(实参1, 实参2...) //在函数调用的小括号里面是实参
// 形参:形式上的参数 函数定义时 传递的参数 当时并不知道什么
// 实参: 实际上的参数 函数调用的时候传递的参数 实参是传递给形参的
// 参数的作用:在函数内部某些值不能固定 我们可以通过参数在调用函数的传递不同值进去
// 3:形参和实参的执行过程
function cook(a) { //形参是接收实参的 a=123459 形参类似于一个变量
console.log(a);
}
console.log(123459);
function getSum(num1, num2) {
console.log(num1 + num2);
}
getSum(1, 9)
getSum(2, 4)
案例:函数封装冒泡排序
function sort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
for (var j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(sort([1, 5, 4, 9, 13, 56, 3]));
var arr1 = sort([1, 5, 4, 9, 13, 56, 3]);
console.log(arr1);
4:函数形参实参个数匹配
// 函数形参实参个数匹配
function getSum(num1, num2) {
console.log(num1 + num2);
}
// 1. 如果实参的个数和形参的个数一致,则正常输出结果
getSum(1, 2); //3
//2:如果实参的个数多于形参的个数 //会取到形参的个数
getSum(1, 2, 3) //3
//3:如果实参的个数小于形参的个数 多于形参定义为underfined 结果为NaN
//形参可以看做是不用声明的变量 num2是一个变量没有接受值 结果就是underfined
getSum(1) //NaN
5:函数的返回值
函数的返回值格式
function 函数名() {
// return 需要返回的结果
}
函数名();
1:我们函数只是实现某种功能,最终的结果需要返回给函数的调用者函数()通过return实现
2:只要函数遇到return 就把后面的结果 返回给函数的调用者
function getResult() {
return 666;
}
getResult(); //getResult
console.log(getResult());
// 函数返回值注意事项
// 1.return 终止函数
function getSum(num1, num2) {
return num1 + num2;
alert('熊二')
}
console.log(getSum(1, 3));
// 2:return 只能返回一个值
function fn(num1, num2) {
return num1, num2;
}
console.log(fn(1, 2));
// 4: 我们的函数如果有return 则返回的是return 后面的值 如果函数没有 return 则返回underfined
6:arguments的使用
当我们不确定 有多少个参数传递的时候,可以用arguments来获取,在js中 arguments实际上它是当前函数的一个内置对象,所有函数都内置了一个arguments对象,arguments对象中存储了传递的所有实参。
arguments是一个类数组对象,包含着传入函数中的所有参数。arguments主要用途是保存函数参数,但是这个对象还有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。
function fn() {
console.log(arguments); //里面储存了所有传递过来的实参
console.log(arguments.length);
console.log(arguments[2]);
}
fn(1, 2, 3)
//伪数组,并不是真正意义上的数组
//1:具有数组length的属性
// 2:按照索引的方式储存
// 他没有真正数组的一些方法 pop()等
案例:
function add(a,b){
console.log(arguments[0],arguments[1],arguments[2],arguments[3]);
console.log(a+b);
}
add(10);
//10 undefined undefined undefined
//NaN
add(10,20);
//10 20 undefined undefined
//30
add(10,20,30);
//10 20 30 undefined
//30
add(10,20,30,40);
//10 20 30 40
//30
7:作用域
1: javaScript 作用域:就是代码名字(变量) 在某个范围内起作用和效果;目的是为了提高 程序的可靠性 更重要的是减少命名冲突
2: js的作用域 (es6 )之前 :全局作用域 局部作用域
3:全局作用域 :整个script标签 或者是一个单独的js文件
4:局部作用域 在函数内部就是局部作用域 这个代码名字只在函数内部起效果和作用
1:局部作用域
在函数中声明的变量只在该函数内部可见。
在 JavaScript函数中声明的变量,会成为函数的局部变量。
函数内部声明的变量,在函数外部不能访问。
var num = 10;
function fn() {
var num = 20;
console.log(num); //20
}
fn()
console.log(num); //10
2:全局作用域
函数之外声明的变量,会成为全局变量。
函数外部声明的变量,在函数内部可以访问。
当函数嵌套,在这个时候,内部函数与外部函数的这个变量就组成了闭包。
//全局作用域:global/window/本文件内
var v1 = 10;
v2 = 20; // 所有末定义直接赋值的变量自动声明为拥有全局作用域
function foo() {
//函数作用域、局部作用域
var a = 3;
console.log(v1, v2);
console.log(this);
}
foo()
console.log(a); // a is not defined
3:变量作用域
// 变量的作用域: 根据作用域的不同我们变量分为全局变量和局部变量
// 1:全局变量:在全局作用域下的变量
var num = 10;
console.log(num);
function fn() {
console.log(num);
}
fn()
// 2:局部变量 在局部作用域下 的变量 后者在函数内部的变量就是 局部变量
function fun() {
var num1 = 20 //num1就是局部变量 只能在函数内部使用
}
fun()
console.log(num1);
4:作用域链
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
// 作用域 :内部函数访问外部函数 采取的就是链式查找的方式来决定取哪个值
var num = 10;
function fn() { //外部函数
var num = 30;
function fun() { //内部函数
console.log(num);
}
}
// 如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺作用域链向父作用域找 //100
console.log(b) // 自由变量,顺作用域链向父作用域找 //200
console.log(c) // 本作用域的变量 //300
}
F2()
}
F1()
8:this的使用
1:对象方法,"this"
在对象方法中, this 指向调用它所在方法的对象。
var user = {
name: "小明",
age: 30,
sayHi() {
// "this" 指的是“当前的对象”
alert(this.name);
}
};
user.sayHi(); // 小明
2:单独使用this
/ 单独使用 this
//2: - 在函数中,this 表示全局对象。
// - 如果单独使用,this 表示全局对象。
// 单独使用 this,则它指向全局对象。
// 在浏览器中,window 就是该全局对象为 [**object Window**]:
// 在node中,指向的是一个{}
var x = this;
console.log(x); //window
3:函数中使用 this(默认)
// 在函数中,函数的所属者默认绑定到 this 上。
// 在浏览器中,window 就是该全局对象为 [**object Window**]:
// 在node中,指向的就是global对象
function myFunction() {
return this;
}
console.log(myFunction()); // window
4:事件中的this
<button onclick="this.style.display='none'"> 点我后我就消失了 </button>
9:预解析
//js引擎运行js 分为两步 :预解析 代码执行
//1.预解析 js引擎会把js 里面所有的 var 还有function 提升到当前作用域的最前面
//代码执行。按照代码书写顺序从上往下执行。
// 2 预解析分为 变量预解析(变量提升) 和函数预解析(函数提升)
// (1)变量提升 就是把所有的变量声明提升到当前的作用域最前面 不提升赋值操作
//函数提升 就是把所有的函数声明提升到当前的作用域最前面 不调用函数
// 1:问
console.log(num);
// 2问
console.log(num);
var num = 10;
//相当于执行以下代码
// var num;
// console.log(num);
// num=10;
// 3问
fn();
function fn() {
console.log(11);
}
//4问
fun();
var fun = function () {
console.log(12)
}
//相当于执行以下代码
// var fun;
// fun();
// fun = function () {
// console.log(22);
// }
10:自由变量
// 首先认识一下什么叫做 ** 自由变量 ** 。如下代码中, `console.log(a)`
// 要得到a变量, 但是在当前的作用域中没有定义a( 可对比一下b)。
// 当前作用域没有定义的变量, 这成为 自由变量。
// 自由变量的值如何得到—— 要到创建这个函数的那个父级作用域寻找,
// 如果没有就一直向上级祖先元素寻找( 这就是所谓的"静态作用域")
var a = 100
function fn() {
var b = 200
console.log(a) // 这里的a在这里就是一个自由变量 // 100
console.log(b)
}
fn()
11:IIFE
// IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。
// 作用
// - 1:页面加载完成后只执行一次的设置函数。
// - 2:将设置函数中的变量包裹在局部作用域中,不会泄露成全局变量
//IIFE的函数调用
(function foo(){
var a = 10;
console.log(a);
})();
1: IIFE的基本使用
// 就像其它任何函数一样,一个立即执行函数也能返回值并且可以赋值给其它变量。
var sum = (function (a,b) {
return a + b;
}(1,2))
console.log(sum);
2:为什么需要IIFE
IIFE
的出现是为了弥补JS在scope
方面的缺陷:JS只有全局作用域(global scope
)、函数作用域(function scope
),从ES6
开始才有块级作用域(block scope
)。那么如何实现作用域的隔离呢?在JS中,只有function
才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。
3:IIFE经典面试题
for (var i = 0; i < 6; i++) {
function output() {
console.log(i); // 为什么输出的是6,而不是0,1,2,3,4,5
// 因为输出的 i 是全局作用域的,当循环结束后 i 的值是 6,所以输出的 i 就是6。
}
}
output()
for (var i = 0; i < 6; i++) {
(function (j) {
console.log(j); //0,1,2,3,4,5
})(i)
// 因为 JS 中调用函数传递参数都是值传递 ,所以当立即执行函数执行时,首先会把参数 i 的值复制一份,然后再创建函数作用域来执行函数,循环5次就会创建5个作用域,所以每个输出访问的都是不同作用域的 i 的值 。
}
4:IIFE常见形式
1.对返回结果不进行处理
(function(形参){
函数体内容
})(实参);
2.对返回结果不进行处理
(function(形参){
函数体内容
}(实参));
3.返回的是一个布尔值,然后进行取反
!function(形参){
函数体内容
}(实参)
4.对于数字返回的是原来的结果,非数字返回NaN
+function(形参){
函数体内容
}(实参)
5.对于数字返回的是正负符号相反,非数字返回NaN
-function(形参){
函数体内容
}(实参)
6.对于数字返回的是正负符号相反再减1,非数字返回-1
~function(形参){
函数体内容
}(实参)
7.返回的结果是undefined
void function(形参){
函数体内容
}(实参)
12:函数调用
调用函数的方式不仅限于()执行,还有其他几种方式
函数名(实参列表);
函数名.call(执行环境对象,实参列表);
函数名.apply(执行环境对象,实参列表数组);
函数名.bind(执行环境对象)(实参列表);
1:函数调用形式
// 直接声明一个函数后调用
function foo() {
console.log('熊大');
}
foo();
// 使用函数的Lambda表达式定义函数,然后调用
var fun =function(){
console.log('熊二');
}
fun();
2:对象的方法
通过建立对象后,来完成对函数的调用
var obj = {
name: '李易峰',
sayName: function () {
console.log(this.name);
}
}
var b = obj.sayName;
b(); //undefined
obj.sayName(); // 李易峰
那我们有什么方法可以让b(),this指向到obj中呢?
有时候我们不得不将这个对象保存到另外的一个变量中,那么就可以通过以下方法。
3:call(执行环境对象,实参列表);
调用call方法,第一个参数就是要把b添加到哪个环境中,简单来说,this就会指向那个对象。
var obj = {
name: '李易峰',
sayName: function () {
console.log(this.name);
}
}
var b = obj.sayName;
b.call(obj); // 李易峰
在使用call调用的时候,还可以传递多个参数
var obj = {
name: '李易峰',
sayName: function (a,b) {
console.log(this.name);
console.log(a,b); // 1,2
}
}
var b = obj.sayName;
b.call(obj,1,2); // 李易峰
4:apply(执行环境对象,实参列表数组);
apply方法和call方法有些相似,它也可以改变this的指向
var obj = {
name: '李易峰',
sayName: function () {
console.log(this.name);
}
}
var b = obj.sayName;
b.apply(obj); // 李易峰
同样apply也可以有多个参数,但是不同的是,第二个参数必须是一个数组,如下:
var obj = {
name: '李易峰',
sayName: function (a,b) {
console.log(this.name);
console.log(a,b); // 100,200
}
}
var b = obj.sayName;
b.apply(obj,[100,200]); // 李易峰
注意:如果call和apply的第一个参数是null,那么this在node环境下指向的是global对象,在HTML中指向的是window对象
var obj = {
name: '李易峰',
sayName: function () {
console.log(this); // global
}
}
var b = obj.sayName;
b.apply(null); // 李易峰
5:bind(执行环境对象)(实参列表);
var obj = {
name: '李易峰',
sayName: function () {
console.log(this.name);
}
}
var b = obj.sayName;
b.bind(obj); // 代码没有被打印,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。
// 新建一个变量c来接收bind修改后的函数
var c = b.bind(obj);
console.log(c); // 发现c是一个[Function: bound sayName]函数
// 执行c
c(); // 李易峰
同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。
var obj = {
name: '李易峰',
sayName: function (a,b,c) {
console.log(this.name);
console.log(a,b,c); // 1,2,3
}
}
var b = obj.sayName;
b.bind(obj); // 代码没有被打印,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。
// 新建一个变量c来接收bind修改后的函数
var c = b.bind(obj, 1, 2);
console.log(c); // 发现c是一个[Function: bound sayName]函数
// 执行c
c(3); // 李易峰
总结:call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。
13:闭包
什么是闭包?
闭包 是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。
简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。
MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
闭包的生成有三个必要条件
函数嵌套函数
内部函数引用了外部函数中的数据(属性、函数)
参数和变量不会被回收
这样就形成了一个不会销毁的函数空间
产生一个闭包
创建闭包最常见方式,就是在一个函数内部创建另一个函数。
function func() {
var a = 1, b = 2;
function closure() {
return a + b;
}
return closure;
}
console.log(func()()); // 3
闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。
在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
//如果我们调用 counter() 多次,count 变量将在同一位置增加到 2,3 等。
在每次
makeCounter()
调用的开始,都会创建一个新的词法环境对象,以存储该makeCounter
运行时的变量。不同的是,在执行
makeCounter()
的过程中创建了一个仅占一行的嵌套函数:return count++
。我们尚未运行它,仅创建了它。因此,
counter.[[Environment]]
有对{count: 0}
词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]]
引用在函数创建时被设置并永久保存。稍后,当调用
counter()
时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于counter.[[Environment]]
:上述例子参考资料
function f1() {
var n = 999;
nAdd = function () { n += 1 }
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
使用闭包注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露,这是IE的BUG。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。多个子函数的scope都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
开发者通常应该都知道“闭包”这个通用的编程术语。
闭包是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。但如上所述,在 JavaScript 中,所有函数都是天生闭包的
也就是说:JavaScript 中的函数会自动通过隐藏的
[[Environment]]
属性记住创建它们的位置,所以它们都可以访问外部变量。在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于
[[Environment]]
属性和词法环境原理的技术细节。参考资料