js-05 函数
一、函数的作用
主要用来储存一段代码,
代码重复使用,大量减少代码量
二、函数声明及调用
- 普通声明:
function name(){content}
- 表达式声明:
var fn = function(){countent}
, - 函数调用:
name()
; - 函数的length属性,为形参的个数;
函数注意事项
函数调用时,是不分先后的;
在任何地方给一个已经声明且赋值的变量再重新声明,重新声明都是无效的。
函数重复声明时,后面的函数会覆盖前面的函数;
如果先用函数表达式声明的,后面声明的普通函数是不能覆盖前面的函数,如果前面是普通声明,后面是表达式声明,则照常执行;
var x = function () { alert(1) };
x(); //1
function x() { alert(3) };
x(); //1
function x() { alert(3) };
x(); //3
var x = function () { alert(1) };
x(); //1
面试题:
var x = 1;
var y = 0;
var z = 0;
function add(n){return n=n+1;}
y = add(x);
function add(n){return n=n+3;}
z = add(x);
s=y+z;
y和z都是4,后面定义的影响到前面定义的;
函数放在其他位置
对象中的函数时
jsvar aa = {name:"张三",fn:function(){循环体}} aa.fn();
经常使用于函数过多,变量名重复;
数组中的函数时
jsvar ary = [1,2,function(){}] // 调用:ary[3]()
三、全局函数
JavaScript 中包含以下 7 个全局函数,用于完成一些常用的功能:
escape( )
、eval( )
、isFinite( )
、isNaN( )
、parseFloat( )
、parseInt( )
、unescape( )
。
注意:setTimeout
不是js的全局函数,是window的全局函数
四、封装函数
写出主体代码;
找出可变的变量,用形参代替;
放到函数中;
调用函数并检查;
五、事件处理函数
obox.onclick = aa
;
事件处理函数当中,函数后面不需要跟小括号;
六、函数参数的类型:
形参和实参
创建:function 函数名(形参){} 多个形参用逗号隔开;
调用:函数名(实参);
注意:
形参比实参多,多出来的形参值为undefined;
实参比形参多,多出来的实参不参与计算,等于没有被识别;
arguments
它是一个对象,是一个类数组(有长度,可以循环,不能使用数组的方法);
类似于数组,数据由实参决定,arguments是函数的内置对象;
当使用一个函数不知道这个函数可以使用哪些形参时,可以利用arguments获取可以使用的形参;可以使用下标方式取值,比如:arguments[0];
js//查询map函数有哪些形参可用; ary.map(function(){ console.log(arguments) })
七、回调函数
把函数作为实参传递到另一个函数当中,为回调函数;
回调函数是某个操作或某个动作做完以后调用的函数。
回调函数实例:
function add(a,b){
return a + b;
}
function fn(num1,num2,func){ //这里fn用来传入一个函数;
return func(num1,num2);
}
console.log(fn(10,20,add)); //这里不能使用小括号,因为在fn函数里面已经调用小括号了;
八、递归函数
在函数中自己调用自己,也可以是一个for循环;
递归一定要有结束条件;
递归: 斐波那契数列第几个数是什么;
function fn(n){
if(n===1 || n===2) return 1; 必须return 1个数值
return fn(n-1) + fn(n - 2);
}
// 求n个数字的和; 递归函数自己调用自己;
function fn(n){
if(n == 1) return 1;
return n + fn(n-1)
}
console.log(3);
// 3 + fn(2)
// 3 + (2 + fn(1))
// 3 + (2 + (1))
尾递归
什么是尾调用:指某个函数的最后一步是调用另一个函数
js// 这种情况叫尾调用 function f(x){ return g(x) } // 这种情况不叫尾调用 function f(x){ let y = g(x) return y // 调用函数g后还有别的操作,不叫尾调用; } // 情况二 也不叫尾调用 function f(x){ return g(x)+1 //调用后还有操作; }
尾递归:函数调用自身,称为递归,如果尾调用自身,称为尾递归
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生栈溢出错误,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出错误;
递归保存n个调用记录,时间复杂度:O(n)
尾递归只保留一个调用记录,时间复杂度:O(1)
js// 递归写法 function fabonacci(n){ if(n===1 || n===2) return 1 return fabonacci(n-1) + fabonacci(n-2) //一直在进行记录,数值过大栈溢出; } // 尾递归写法; function fabonacci(n,start=1,total=1){ if(n===1 || n===2) return total return fabonacci(n-1, total, start+total) } /** * 尾递归斐波那契分析: * 第一个为次数,第二个为保留上一次的结果,第三个用于返回的结果; * 假如传入10,第一次为 9 1 2 * 第二次为 8 2 3 * 第三次为 7 3 5 * 最后一位依次为: 2 3 5 8 * 在es6中,只要使用尾递归,就不会发生栈溢出,相对节省内存; */ function fib(n) { let p = 0, c = 1; while(n--) [p, c] = [c, p + c] return p; }
九、作用域
每个函数都有自己的执行期上下文,每个执行期上下文都有自己的变量对象;
全局变量的作用域
用var声明的变量,可以在全局使用;
除了在函数内部定义的变量都是全局变量;
全局作用域声明的变量、函数会变成window的方法或属性。
全局变量如果页面不关闭,全局变量就不会释放,就会占空间,消耗内存,所以尽可能的在函数里面使用局部变量;
局部变量
在函数内部定义的变量是局部变量,
块级作用域
花括号里面声明的变量只能在花括号当中使用,比如:if语句和for循环语句;
但是在js当中,外面也能使用花括号的变量,因为在js里没有块级作用域,只有函数除外,在es6里面let声明的变量有作用域;
隐式全局变量
声明变量没有var,就叫隐式全局变量;
如果在函数里面声明变量没有var,函数执行之后,外面也能拿到函数里面的变量;
注意:
全局变量是不能被删除的,隐式全局变量时可以被删除的;
定义变量使用var是不能被删除的,没有var是可以删除的;
delete 变量
删除变量。
[[scope]]
(这里跳过,建议去看视频,推荐渡一教育的视频,作用域讲的比较详细)
作用域是因为由函数的产生而产生的作用域;
每个javascript函数也是一个对象,函数叫类对象,函数可以访问它的name属性和prototype属性,但有些属性是不能访问的,比如[[scope]]
,这些属性仅javascript引擎存取;
AO(Active Object)
是执行期上下文,记录了该环境中所有声明的参数,变量和函数;
GO(Global Object)
是全局产生的执行期上下文;
全称:VO(variable)
,AO是正在执行的VO,GO全局中的VO;
[[scope]]
是隐式的属性,不能拿出来,我们不能访问,系统可以用,里面存储了执行期上下文(AO)的集合。
执行期上下文:
当一个函数执行时,会创建一个称为执行期上下文(AO)的内部对象,
一个执行期上下文定义了一个函数执行时的环境,
函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,
当函数执行完毕,它所产生的执行上下文被销毁;
查找变量,从作用域链的顶端依次向下查找;
作用域链:[[scope]]
中存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;
每一个函数都有一个执行期上下文的集合,叫作用域链;
当一个函数被创建时,就可以访问fn.name
,fn.[[scope]]
fn.[[scope]]
里面存放了一个0,0指向GO,global Object;
一个函数被定义时,可以访问[[scope]]
,里面储存的是GO,一个函数执行之后会产生一个新的执行期上下文,存的是AO,GO被压下去了,变成了第1位,AO在第0 位,在最顶端;
当函数里面还有函数时,子函数可以访问父函数的劳动成果,可以访问父函数的[[scope]]
,里面还存有自己的执行期上下文(AO);
查找一个变量,scope会从顶端开始查找作用域链,如果顶端查找不到就会查找下一个;
函数在被调用执行时,会创建一个当前函数的执行期上下文,在该执行期上下文的创建阶段,变量对象、作用域、闭包、this指向会分别被确定,而一个JavaScript程序中一般来说会有多个函数,JavaScript引擎使用调用栈来管理这些函数的调用顺序,函数调用栈的调用顺序与栈数据结构一致;
例子:
a定义之后会产生一个scope,scope里面储存的是全局的执行上下文GO,之后a被执行,产生了一个AO,被放到了scope的最顶端,形成了一个新的作用域链,
function a(){
function b(){
var b = 234;
}
var a = 123
b();
}
var glob = 100
a()
javascript变量的生命周期
JavaScript 变量生命周期在它声明时初始化。
局部变量在函数执行完毕后销毁。
全局变量在页面关闭后销毁。
十、作用域链:
从内到外查找,先找自己局部区域有没有,再往外一层
作用域链:[[scope]]中所存储的执行期上下文(AO)对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;
十一、预解析(变量提升)
执行代码前,会先找页面中的(var 和 function),找出来之后并把变量赋值为undefined,函数永远是最后提升的
逐行解析:遇到赋值表达式时,会将值重新赋值给变量;
变量的提升:只会在当前的作用域中提升,提升到当前的作用域的最上面; 函数中的变量只会提前到函数的作用域最前面,不会提升到外面去;
作用域预解析必背四个步骤
创建AO/GO对象,
变量提升并赋值undefined
形参实参相统一;
函数永远是最后提升的,函数最后提升到最上面;
// 由于函数最后提升,先提升var,赋值undefined,之后提升function,a被赋值函数,因此第一个打印函数;
// 之后a又被赋值,因此之后的值一直是a的,function已经被提升到最上面了;
console.log(a);
function a(){
console.log("aaaa");
}
var a = 1;
function a(){
console.log('bbbb')
}
console.log(a);
十二、this
this定义
函数中的this指向,不是在定义的时候确定的,而是在调用的时候确定的;
当事件放到循环里面时,是拿不到i的,需要用this代替这个,比如:
for(var i=0;i<oli.length;i++){
oli[i].onmouseover = function(){
this.style.display = "block"; //事件里面的i是无法获取到会报错的;这个this代表划过的这个;
}
this可以在js任何地方使用,位置不同指向不同,
全局的任何地方都可以调用,window是页面中最大的对象,
this指向
(1)触发当前事件的对象;
(2)普通函数的this指向window;
(3)构造函数的this指向实例对象;
(4)方法中的this指向实例对象;
(5)原型中的this指向实例对象
(6)定时器中的this指向window;
(7)函数作为某个对象的方法时,this指向这个对象;
(8)函数作为某个数组的数据时,this指向这个数组;
总结:执行代码前,会把变量的声明提前,变量值为undefined,当遇到变量值时,并给变量赋值;
面试题2:
fn();
console.log(c);
console.log(b);
console.log(a);
function fn(){
var a=b=c=1;
console.log(c);
console.log(b);
console.log(a);
}
解答: 执行完是这样的:
function fn(){
var a=undefined;
a = 1;
b=1;
c=1;
} //只有b和c没有var,是隐式全局变量,所以外面可以拿到值,a被声明了是局部变量,所以a报错了;
相同题:
var a,b
(function(){
alert(a);
alert(b);
var a=b=3;
alert(a);
alert(b);
}());
alert(a);
alert(b);
面试题3:
f1();
var f1=function(){
console.log(a);
var a = 10;}
解答:
var f1;
f1();
f1=function(){}; //var声明的函数,函数为表达式,是赋值的,变量名会提前,;f1找不到;
面试题4:
var color = 'green';
var test4399 = {
color:'blue',
getColor:function(){
var color = 'red';
alert(this.color);
}
}
var getColor = test4399.getColor;
getColor();
test4399.getColor();
解答:
第一个执行结果是:window.getColor() this指向window,是green;
第二个执行结果:test4399.getColor();this指向test4399,是blue;
十三、函数返回值
- 函数调用以后的结果,就是return后面的值;
- 如果没有return,则返回undefined;
- return 后面的代码不会再执行;
- return后面有什么,最终返回结果就是什么;
- 直接调用函数,需要使用一个变量接收一下函数;
- 如果函数return后面没有任何值,返回结果将会是undefined;
总结:使用return就必须有返回值,要么就永远不使用;
return 返回多个值时,只能返回最后一个值,如果需要返回多个值可以放到一个数组当中,或对象中返回;
返回值的类型可以是js中数据类型中的任何一个;
面试题:
var x = 1, y = z = 0;
function add(n) {
n = n + 1;
};
y = add(x);
function add(n) {
n = n + 3;
};
z = add(x);
console.log(x,y,z);
解答: 两个函数都没有return 返回值,所有x和y都为undefined;
十四、函数注释
实例:
/**
*
*@param num {Number} 数字
*@param str {String} 名称
*@return {Boolean} true:成年 false:未成年
* @type { import('vite').UserConfig } // 使用ts类型
*/
function check(num,str){
if(num === 18) {
return true
}
}
其他函数调用方式
function fn(one, two, three){
console.log(one, two, three)
}
const a = 'hh'
const b = 'ss'
fn`this a is ${a} and b is ${b}`
使用模板字符串调用函数时:
- 第一个参数:数组,包含模板字符串分割的字符串
- 第二个参数:模板字符串的第一个变量,后面的变量以此类推
高频面试题
● 写一个获取非行间样式的函数
● 说说你对作用域链的理解?
● var x = 1, y = z = 0; function add(n) { n = n + 1; }; y = add(x); function add(n) { n = n + 3; }; z = add(x); console.log(x,y,z);
● 请解释变量提升?
● 函数声明和函数表达式声明的区别?
● JavaScript 两种变量范围有什么不同?