JS基础(二)函数专题

函数专题

函数是指一段在一起的、可以做某一件事儿的代码。可以随时运行,可以重复使用。

创建函数

1
2
3
4
function fn[函数名](){
//=>[函数体]
//实现功能的具体JS代码
}

函数执行

fn(); //=>把创建的函数执行,而且这个函数可以执行很多次

每一次执行都相当于把函数体中实现功能的JS代码重复执行了一遍

在真实项目中,我们一般都会把实现一个具体功能的代码封装在函数中:
1、如果当前这个功能需要在页面中执行多次,不封装成为函数,每一次想实现这个功能,都需要重新把代码写一遍;而封装在一个函数中,以后想实现这个功能,只需要把函数重新的执行即可,提高了开发效率;
2、封装在一个函数中,页面中就基本上很难出现重复一样的代码了,减少了页面中代码的冗余度,提高了代码的重复利用率:低耦合高内聚

我们把以上的特点称为函数封装(OOP面向对象编程思想,需要我们掌握的就是类的继承、封装、多态)

JS中函数的核心原理

函数作为JS中引用数据类型中的一种,也是按照引用地址来操作的

1、函数执行首先会形成一个新的私有作用域,目的是为函数体中的代码提供一个执行的环境(而且这个环境是私有的)

2、把创建时候存储的代码字符串copy一份到自己的私有作用域中,然后把字符串转换为JS表达式,再然后依次自上而下执行
目的:把之前存储的实现具体功能的JS代码执行

函数执行步骤

  • 形参赋值
  • 私有作用域中的变量提升
  • 把之前创建时候存储的那些JS代码字符串,拿到私有作用域中,然后把它们变为JS表达式从上到下执行
  • 私有作用域是否销毁的问题[根据栈内存是否被占用]

函数中的变量提升

在当前作用域中,js代码自上而下执行之前,浏览器首先会把所有带var/function关键字的进行提前的声明或者定义

声明(declare):var num; 在当前作用域中吼一嗓子我有num这个名了
定义(defined):num=11; 把声明的名字赋一个值

带var关键字的只是提前的声明一下;带function关键字的在变量提升阶段把声明和定义都完成了;

变量提升详解

对于带var和function关键字在与解释的时候操作不一样

  • var 在预解释的时候只是提前声明
  • function 在预解释时提前将声明和定义都完成了;

1、.预解释只发生在当前作用域中,开始只对window下的预解释,只有在函数执行时,才会对函数体中的进行预解释
2、在if判断中,在预解释时,不管你条件是否成立,都要将带var的进行预解释(在老版本浏览器中,函数会被声明和定义,新版本浏览器中,函数只会被声明)
3、var fn=function(){},在匿名函数值函数表达式中只对等号左边进行预解释,右边是值,不参加预解释

真实项目中,应用这个原理,我们创建函数的时候可以使用函数表达式的方式:
1、因为只能对等号左边的进行提升,所以变量提升完成后,当前函数只是声明了,没有定义,想要执行函数只能放在赋值的代码之后执行(放在前面执行相当于让undefined执行,会报错的)
2、这样让我们的代码逻辑更加严谨,以后想要知道一个执行的函数做了什么功能,只需要向上查找定义的部分即可(不会存在定义的代码在执行下面的情况)

4、function fn(n){};(10); 自执行函数,在全局作用域下不进行预解释,在执行到该位置是定义和执行一起完成。
5、当函数中有return时,return下面的函数不执行,但是会预解释;return后面跟的是返回值,所以不进行预解释。
6、在预解释中,如果名字已经声明过了,后面就不重新声明,但会重新赋值;

如何区分私有变量和全局变量

  • 在全局作用域下声明的都是全局变量
  • 在私有作用域下声明的变量和函数的形参都是私有 变量

在私有作用域中,我们代码执行的时候遇到一个变量,首先我们要确定他是否为私有变量,如果是私有变量,那么他和外面没有任何关系;如果不是私有变量,(不加var)那么就要向他的上级查找,一直到window为止;

看当前函数在哪个作用域定义,那么他的上级作用于就是谁,跟在哪里运行没有关系

在全局作用域中带var和不带var的区别

1、带var的可以先进行预解释,所以在赋值前执行不会报错;不带var的没有进行预解释,在前面执行会报错;
2、num=12;console.log(num2); ==>12 //含义:相当于给window增加一个属性,属性名=num,属性值=12;
3、var num=12;console.log(num); => 12// 首先给全局作用域增加一个全局变量,而且也为window增加一个属性

in :判断变量是否存在当前作用域中;

++i和i++的区别

都是自增1的意思
在运算中不同点i++是先进行运算,再自增
++i是先自增1再运算

作用域链

函数执行形成一个私有的作用域(保护私有变量),进入到私有作用域中,首先变量提升(声明过的变量是私有的),接下来代码执行
1、执行的时候遇到一个变量,如果这个变量是私有的,那么按照私有处理即可
2、如果当前这个变量不是私有的,我们需要向它的上级作用域进行查找,上级如果也没有,则继续向上查找,一直找到window全局作用域为止,我们把这种查找机制叫做作用域链
1)如果上级作用域有这个变量,我们当前操作的都是上级作用域中的变量(假如我们在当前作用域把值改了,相当于把上级作用域中的这个值给修改了)
2)如果上级作用域中没有这个变量(找到window也没有):
变量 = 值 :相当于给window设置了一个属性,以后再操作window下就有了
alert(变量):想要输出这个变量,但是此时是没有的,所以会报错

闭包

函数执行会形成一个私有的作用域,让里面的私有变量和外界互不影响(相互不干扰、外面的无法直接获取里面的变量值),此时我们可以理解为私有作用域把私有变量保护起来的,我们把这种保护机制称之为闭包
闭包的作用:
保护:私有作用域把私有变量保护起来的
保存:函数执行形成一个私有作用域,函数执行完成,形成的这个栈内存一般情况下都会自动释放
但是还有其他情况:函数执行完成,当前私有作用域(栈内存)中的某一部分内容被栈内存以外的其它东西(变量/元素的事件)占用了,当前的栈内存就不能释放掉,也就形成了不销毁的私有作用域(里面的私有变量也不会销毁)

堆内存

所有的引用数据类型,它们需要存储的内容都在堆内存中(相当于一个仓库,目的是存储信息)

对象会把键值对存储进来
函数会把代码当做字符串存储进来
释放:如果当前的堆内存被变量(或者函数以及元素事件等)占用了(占用了:堆内存地址赋值给变量了),此时的堆内存是有用的,不能销毁;我们想要手动释放堆内存,只需要让存储地址的变量等于其它值即可(最好等于null,null是空对象指针,本意就是不指向任何的堆内存);

函数中的形参和实参

形参:相当于函数提供的入口,需要用户执行函数的时候把需要的值传递进来,形参是个变量,用来存储和接收这些值

实参:用户执行的时候传递给形参的具体值

  • 在非严格模式下:函数的形参和实参有一一对应的映射关系;当没有传递实参时,这种映射关系被切断,严格模式下不存在映射关系
1
2
3
4
5
6
7
8
9
//=>随便求出两个数的和
function sum(num1,num2){//=>num1/num2就是形参变量(类似于var了一下)
var total = num1+num2;
total*=10;
total=total.toFixed(2);
console.log(total);
}
sum(10,20);//->10/20是实参 num1=10 num2=20
sum(10); //->num1=10 num2=undefined 定义了形参但是执行的时候没有传递实参,默认实参的值是undefined 此时num2与实参的映射关系被切断
1
2
3
4
5
6
7
8
9
10
11
12
function sum(num1, num2) {
//=>如果有一个值没有传递的话,我们为了保证结果不是NaN,我们为其设置一个默认的值:0
//=>容错处理
num1 = num1 || 0;
num2 = num2 || 0;
var total = num1 + num2;
total *= 10;
total = total.toFixed(2);
console.log(total);
}
sum(10, 20);
//在非严格模式下,函数的两个形参可以重名,但是函数只会接收后一个参数传的值,如果只传入一个实参,则如下图所示: 在严格模式下,形参不可以重名

arguments实参集合

  • 当我们不知道用户具体要传递几个值的时候(传递几个值都行),此时我们无法设置形参的个数;遇到此类需求,需要使用函数内置的实参集合:arguments
    1、arguments只有函数才有
    2、不管执行函数的时候是否传递实参,arguments天生就存在,没有传递实参ARG是个空的集合,传递了ARG中包含了所有传递的实参值
    3、不管是否设置了形参,ARG中始终存储了所有的实参信息

  • arguments是一个类数组集合
    1、以数字作为索引(属性名),从零开始
    arguments[0] 第一个实参信息
    arguments[2] 第三个实参信息
    2、有一个length的属性,存储的是当前几个的长度(当前传递实参的个数)
    arguments.length
    arguments[‘length’]
    arguments.callee:存储的是当前函数本身
    arguments.callee.caller:存储的是当前函数在哪执行的(宿主函数),在全局作用域下执行的,结果是null

在非严格模式下,agruments与形参有一一对应的关系,但当没有传入实参时,这种映射关系会被切断,
在严格模式下,agruments与形参的映射关系也被切断了

1
2
3
4
5
6
"use strict"; //=>在JS代码执行之前加入这句话:开启JS的严格模式
function sum() {
console.log(arguments.callee);//=>Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
}
sum(10, 20, '珠峰', {name: '珠峰'});
//=>arguments.callee或者arguments.callee.caller一般真正项目中很少使用:因为在严格的JS模式下不允许我们使用这两个属性,然而现有项目大部分都是基于严格模式来的

JS中的返回值 return

  • 返回值是函数提供的一个出口:我们如果想在外面使用函数私有的一些信息,那么就需要通过return,把这些信息返回出来供外面使用 - RETURN后面跟着的都是值(返回的都是值)
  • 如果函数中没有写RETURN或者RETURN后面啥也没有,默认返回的结果就是undefined
  • 在函数体中遇到RETURN后,RETURN后面的代码都不在执行了
    1
    2
    3
    4
    5
    6
    7
    8
    function sum() {
    var total = null;
    return total;//=>RETURN后面跟着的都是值(返回的都是值):此处不是把TOTAL变量返回,而是把TOTAL存储的值返回而已 <=> RETURN 60;
    //return 1+1; //<=> RETURN 2;
    }
    console.log(sum(10, 20, 30));
    //=>sum:代表的是函数本身
    //=>sum():让函数先执行,代表的是当前函数返回的结果(RETURN 后面是啥,相当于函数返回的是啥)
1
2
3
4
5
6
7
8
9
10
function sum() {
var total = null;
for (var i = 0; i < arguments.length; i++) {
var cur = Number(arguments[i]);
!isNaN(cur) ? total += cur : null;
}
return total;
}
var total = sum(10, 20, 30);//=>外面是全局下的TOTAL和函数中的TOTAL没有必然的联系
console.log(total.toFixed(2));

JS中的匿名函数
没有名字的函数

  • 函数表达式:把一个函数作为值赋值给一个变量或某个事件(函数表达式右边的都是匿名函数)
  • 自执行函数:创建函数和执行函数放在一起了,创建完成立马执行
    1
    2
    3
    oBox.onclick = function(){
    //=>把一个没有名字的函数(有名字也无所谓)作为值赋值给一个变量或者一个元素的某个事件等:`函数表达式`
    }
1
2
3
4
5
6
7
8
9
;(function(n){
//=>创建函数和执行函数放在一起了,创建。,完成立马执行:·自执行函数·
//n形参 n=10
})(10);
//=>以下都是自执行函数,符号只是控制语法规范,
~function(n){}(10);
-function(n){}(10);
+function(n){}(10);
!function(n){}(10);

函数的三种角色和call、apply、bind

第一种角色:普通函数
栈内存(私有作用域)
作用域链
形参
arguments
return

第二种角色:类

实例
私有和公有属性
prototype
proto

第三种角色:普通对象
键值对操作

三种角色之间没有直接的关系

1
2
3
4
5
6
7
function Fn(){
var name='珠峰培训';
this.age=8;
}
Fn.prototype.say=function(){}//原型上的方法
Fn.eat=function(){}//普通函数的方法
var f = new Fn();

阿里超经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
1
2
3
4
5
6
7
Foo.getName(); //执行Foo.getName 输出2
getName(); // 执行var getName 输出4
Foo().getName();//执行function Foo(),(getName是公有属性,相当于给getName重新赋值) 输出1
getName(); //由于上一轮的重新赋值,输出1
new Foo.getName();//返回Foo.getName()的一个实例,输出2
new Foo().getName();//先new Foo(),返回foo的一个实例,在执行实例.getName(),=》3;
new new Foo().getName();// 先new Foo(),返回foo的一个实例,再返回this.getName()的一个实例 ,3;

call apply bind

都是天生自带的方法(Function.prototype),所有的函数都可以调取这三个方法 三个方法都是改变THIS指向的

call

fn.call(context,para1,…)
把fn方法立即执行,并且让fn方法中的this变为context,而para1…都是给fn传递的实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//=>非严格模式
function fn(num1,num2){
console.log(this);
}
var obj={fn:fn};
fn();//=>this:window
obj.fn();//=>this:obj
var opp={};
//opp.fn();//=>报错:opp中没有fn这个属性
fn.call(opp);//=>this:opp num1&&num2都是undefined
fn.call(1,2);//=>this:1 num1=2 num2=undefined
fn.call(opp,1,2);//=>this:opp num1=1 num2=2
//->CALL方法的几个特殊性
fn.call();//=>this:window num1&&num2都是undefined
fn.call(null);//=>this:window
fn.call(undefined);//=>this:window
//=>JS严格模式下
"use strict";
fn.call();//=>this:undefined
fn.call(undefined);//=>this:undefined
fn.call(null);//=>this:null

apply

apply的语法和call基本一致,作用原理也基本一致,唯一的区别:apply把传递给函数的实参以数组形式存放(但是也相当于在给函数一个个的传递实参值)

1
fn.call(null,10,20,30);
1
fn.apply(null,[10,20,30]); //=>传递给fn的时候也是一个个的传递进去的

bind

也是改变THIS的方法,它在IE6~8下不兼容;它和call(以及apply)改变this的原理不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn.call(opp,10,20); //=>把fn执行,让fn中的this变为opp,并且把10&&20分别传递给fn
fn.bind(opp,10,20); //=>预先让fn中的this指向opp,并且把10和20预先传递给fn,此时的fn没有被执行(只有当执行的时候this和实参才会起到应有的作用)
//=>需求:点击box这个盒子的时候,需要执行fn,并且让fn中的this指向opp
oBox.onclick=fn; //=>点击的时候执行了fn,但此时fn中的this是oBox
oBox.onclick=fn.call(opp); //=>绑定事件的时候就已经把fn立即执行了(call本身就是立即执行函数),然后把fn执行的返回值绑定给事件
oBox.onclick=fn.bind(opp);
//=>fn.bind(opp):fn调取Function.prototype上的bind方法,执行这个方法返回了一个匿名函数
/*
* function(){
* fn.call(opp);
* }
*/
oBox.onclick=function(){
//=>this:oBox
fn.call(opp);
}

回调函数

凡是在函数执行的某一阶段需要完成某件不确定的事情,可以利用回到函数机制,把要处理的事情当做函数传进来,传进来的这个参数就是回调函数

  • 我们可以在函数中根据需要随时使用回调函数
  • 我们还可以给回调函数传递参数
  • 我们还可以把回调函数中的this进行修改
  • 我们还可以接收回调函数的返回值

回调函数的this指向

回调函数中的this一般都是window(严格模式下是undefined)setTimeout&&setInterval默认执行主体都是window,(严格模式下也是同样的)

forEach和map当传第二个参数时,会改变this指向,this会指向传递的第二个参数(some.filter,find,every这些方法的第二个参数都是改变this指向的)

我们一般在执行回调函数的时候,没有特意指定执行主体,所以默认一般都是window。

坚持原创技术分享,您的支持将鼓励我继续创作!