函数专题
函数是指一段在一起的、可以做某一件事儿的代码。可以随时运行,可以重复使用
创建函数
function fn[函数名](){
//=>[函数体]
//实现功能的具体JS代码
}
函数执行
在真实项目中,我们一般都会把实现一个具体功能的代码封装在函数中:
1、如果当前这个功能需要在页面中执行多次,不封装成为函数,每一次想实现这个功能,都需要重新把代码写一遍;而封装在一个函数中,以后想实现这个功能,只需要把函数重新的执行即可,提高了开发效率;
2、封装在一个函数中,页面中就基本上很难出现重复一样的代码了,减少了页面中代码的冗余度,提高了代码的重复利用率:低耦合高内聚
我们把以上的特点称为
函数封装
(OOP面向对象编程思想,需要我们掌握的就是类的继承、封装、多态)
JS中函数的核心原理
函数作为JS中引用数据类型中的一种,也是按照引用地址来操作的
var obj={
name:'韩梅梅',
age:8
};
function sum() {
var total = 1 + 1;
total *= 20;
console.log(total.toFixed(2));
}
sum();
首先会在当前作用域中声明一个函数名
(声明的函数名和使用var声明的变量名是一样的操作:var sum; function sum; 这两个名字算重复声明了)浏览器首先会开辟一个新的内存空间
(分配一个16进制地址),把函数体中写好的代码当做普通字符串
存储在这个内存空间中(创建一个函数如果不执行,函数没有意义)- 把
内存空间的地址赋值给之前声明的那个函数名
函数执行
函数执行的时候也有属于自己的一系列操作
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(变量):想要输出这个变量,但是此时是没有的,所以会报错
闭包
函数执行会形成一个私有的作用域,让里面的私有变量和外界互不影响(相互不干扰、外面的无法直接获取里面的变量值),此时我们可以理解为私有作用域把私有变量保护起来的,我们把这种保护机制称之为
闭包
闭包的作用:
保护:私有作用域把私有变量保护起来的
保存:函数执行形成一个私有作用域,函数执行完成,形成的这个栈内存一般情况下都会自动释放
但是还有其他情况:函数执行完成,当前私有作用域(栈内存)中的某一部分内容被栈内存以外的其它东西(变量/元素的事件)占用了
,当前的栈内存就不能释放掉,也就形成了不销毁的私有作用域
(里面的私有变量也不会销毁)
栈内存
作用域(全局作用域/私有作用域):提供一个供JS代码执行的环境(基本数据类型值都是直接的存储在栈内存中)
全局作用域(栈内存):
形成:浏览器渲染页面,首先就会形成一个全局作用域
销毁:关闭当前页面(F5刷新页面:先把页面关闭,然后再重新打开)私有作用域(栈内存):
形成:函数执行会形成私有的作用域
销毁:一般情况下,函数体中的代码执行完成,形成的栈内存会立即释放;当然也有不释放的情况。
堆内存
所有的引用数据类型,它们需要存储的内容都在堆内存中(相当于一个仓库,目的是存储信息)
- 对象会把键值对存储进来
- 函数会把代码当做字符串存储进来
释放:如果当前的堆内存被变量(或者函数以及元素事件等)占用了(占用了:堆内存地址赋值给变量了),此时的堆内存是有用的,不能销毁;我们想要手动释放堆内存,只需要让存储地址的变量等于其它值即可(最好等于null,null是空对象指针,本意就是不指向任何的堆内存);
函数中的形参和实参
形参:相当于函数提供的入口,需要用户执行函数的时候把需要的值传递进来,形参是个变量,用来存储和接收这些值
实参:用户执行的时候传递给形参的具体值
- 在非严格模式下:函数的形参和实参有一一对应的映射关系;当没有传递实参时,这种映射关系被切断,严格模式下不存在映射关系
//=>随便求出两个数的和
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与实参的映射关系被切断
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与形参的映射关系也被切断了
"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后面的代码都不在执行了
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 后面是啥,相当于函数返回的是啥)
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中的匿名函数
没有名字的函数
函数表达式
:把一个函数作为值赋值给一个变量或某个事件(函数表达式右边的都是匿名函数)自执行函数
:创建函数和执行函数放在一起了,创建完成立马执行
oBox.onclick = function(){
//=>把一个没有名字的函数(有名字也无所谓)作为值赋值给一个变量或者一个元素的某个事件等:`函数表达式`
}
;(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__第三种角色:普通对象
键值对操作三种角色之间没有直接的关系
function Fn(){
var name='珠峰培训';
this.age=8;
}
Fn.prototype.say=function(){}//原型上的方法
Fn.eat=function(){}//普通函数的方法
var f = new Fn();
阿里超经典面试题
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);
}
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传递的实参
//=>非严格模式
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把传递给函数的实参以数组形式存放(但是也相当于在给函数一个个的传递实参值)
fn.call(null,10,20,30);
fn.apply(null,[10,20,30]); //=>传递给fn的时候也是一个个的传递进去的
bind
也是改变THIS的方法,它在IE6~8下不兼容;它和call(以及apply)改变this的原理不一样
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.οnclick=fn; //=>点击的时候执行了fn,但此时fn中的this是oBox
oBox.οnclick=fn.call(opp); //=>绑定事件的时候就已经把fn立即执行了(call本身就是立即执行函数),然后把fn执行的返回值绑定给事件
oBox.οnclick=fn.bind(opp);
//=>fn.bind(opp):fn调取Function.prototype上的bind方法,执行这个方法返回了一个匿名函数
/*
* function(){
* fn.call(opp);
* }
*/
oBox.οnclick=function(){
//=>this:oBox
fn.call(opp);
}
回调函数
凡是在函数执行的某一阶段需要完成某件不确定的事情,可以利用回到函数机制,把要处理的事情当做函数传进来,传进来的这个参数就是回调函数
- 我们可以在函数中根据需要随时使用回调函数
- 我们还可以给回调函数传递参数
- 我们还可以把回调函数中的this进行修改
- 我们还可以接收回调函数的返回值
回调函数的this指向
回调函数中的this一般都是window(严格模式下是undefined)setTimeout&&setInterval默认执行主体都是window,(严格模式下也是同样的)forEach和map当传第二个参数时,会改变this指向,this会指向传递的第二个参数(some.filter,find,every这些方法的第二个参数都是改变this指向的)
我们一般在执行回调函数的时候,没有特意指定执行主体,所以默认一般都是window。