JavaScript高级程序设计学习笔记(七) — 函数

函数

1.箭头函数

//箭头函数
let arr = (a,b)=>{
    return a+b;
}
//普通
let arr2 = function(a,b) {
    return a+b;
}
//如果只有一个参数,可以不是用括号,在没有参数或者多个参数的时候必须使用括号
let double = x =>{ return 2*x; };
let add = (x,y)=>{ return x+y; };
let sum = ()=>{ return 1; };

箭头函数也可以不加大括号,如果不加大括号只能加一行代码,比如一个赋值操作,或者一个表达式,省略大括号的时候会自动返回表达式的值

let double = (x)=>2*x;
//无效的写法
let double = (a,b)=>return a+b;
  • 缺点:不能使用argumentssuper,new.target,也不能用作构造函数,而且也没有prototype属性

2.函数名

函数名是指向函数的指针,所以一个函数可能有多个函数名,使用不带括号的函数名会访问函数指针,而不会执行函数

es6的所有函数对象都会有一个只读的name属性,其中包括函数的信息

3.参数

js中不关心传入参数的个数,也不关心数据类型,假如定义函数时,要求传入两个参数,但是调用函数的时候就不一定非得传入两个参

  • 因为函数的参数在内部表现为一个数组,函数被调用的时候会接受一个数组,在使用function关键字定义函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值

arguments是一个类数组对象,可以使用中括号进行访问

function say() {
    console.log("hello"+arguments[0]);
}

类函数重载的写法:不计较参数的个数

function add() {
    if(arguments.length == 1){
        console.log(arguments[0]+10);
    }else if(arguments.length == 2){
        console.log(arguments[0]+arguments[1]);
    }
}
add(1);
add(1,12);
  • 如果函数是箭头函数,不能使用arguments关键字访问,只能通过定义的命名参数访问
function foo() {
    let bar = () =>{
        console.log(arguments[0]);
    };
    bar();
}
foo();

4.重载

js里面没有函数重载,如果在js中定义了两个同名函数,后定义的会覆盖先定义的,可以用条件判断arguments来模拟函数重载

5.默认参数值

参数使用默认值

function add(name="hello") {
    console.log(name);
}
add();  //hello

给参数传undefined相当于没有传值,但是想要在后几个参数传值的时候需要使用

arguments对象的值不反应参数的默认值,只会记录传入的参数值

在设置参数默认值的时候,后面的参数可以调用前面参数(参数名而不是参数的值),即前面定义的补鞥呢引用后面定义的,后面定义的可以引用前面定义的

function(name="hello",h=name) {
    console.log(name);  //hello
    console.log(h);  //hello
}

参数存在于自己的作用域中,他们不能引用函数体的作用域:

//如果不传入第二个参数会报错
function making(name="tom",sup=defa) {
    let defa="hello";
    console.log(name);
    console.log(sup);
}

6.参数拓展与收集

再给函数传参时,可能需要分别传入数组的值

let values = [1,2,3,4];
function getSum() {
    let sum = 0;
    for(let i = 0;i<arguments.length;i++) {
        sum+= arguments[i];
    }
    return sum;
}

这个函数希望将所有数组里面的值累加,es6中可以使用拓展操作符实现

对可迭代对象应用拓展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入,可以将可迭代对象拆分,并将可迭代返回的每个值传入

比如:使用拓展操作符可以将前面例子中的数组像这样直接传给函数

console.log(getSum(...values));  //10
console.log(-1,...values)

函数的默认参数只有在函数被调用的时候才会求值,不会在函数定义的时候求值,而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用(箭头函数也可以使用)

默认参数作用域和暂时性死区

因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的

给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数

参数初始化遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的

7.函数声明与函数表达式

js引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义

函数表达式必须等到代码执行到他那一行,才会在执行上下文中生成函数定义

//没问题
console.log(sum(10,10));
function sum(num1,num2) {  //函数声明早已经加载完
    return num1+num2;
}

因为函数声明已经在代码执行之前被读取并添加到执行上下文照片呢个,这个过程叫做函数声明提升

但是使用函数表达式并不能获得提升

8.函数作为值

不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数

  • 函数作为参数传给另一个函数
function callSomeFunction(someFunction,someArguments) {
    return someFunction(someArguments);
}
function add(num) {
    return num+10;
}
let result = callSomeFunction(add,10);
console.log(result);  //20
  • 在一个函数中返回另一个函数
function createComparisonFunction(propertyName) {
    return function(object1,object2){
        let value1 = object[propertyName];
        let value2 = object[propertyName];

        if(value1<value2){
            return -1;
        }else if(value1>value2) {
            return 1;
        }else {
            return 0;
        }
    };
}

let date = [
    { name:"zach",age:20 },
    { name:"ha",age:22 }
];
date.sort(createComparisonFunction("name"));
console.log(date[0].name);  //ha

9.函数内部

函数内部包含两个特殊的对象:argumentsthis(es5),还有一个特殊的属性:new.target(es6)

arguments

arguments是一个类数组对象,包含调用函数时传入的所有参数,arguments还有一个属性callee,是一个指向arguments对象所在函数的指针:

function fac(num) {
    if(num<1=) {
       return 1;
    }else {
        return num*arguments.callee(num-1);  
        //此时更换函数名也能正常执行这段代码
    }
}

this

  • 在标准函数中,this引用的是吧函数当成方法调用的上下文对象,这时候通常称为this值
window.color = "red";
let o = {
	color:"blue"
};
function say() {
    console.log(this.color);
};
say();  //"red"
o.say = say;
o.say();  //"blue"
  • 箭头函数:this引用的是定义箭头函数的上下文
window.color = 'red';
let o = {
    color:'blue'
};
let say = ()=>console.log(this.color);
say();  //'red'
o.say = say;
o.say();  //'red'

上面代码中this指向的都是window对象,因为箭头函数是在window下定义的

在事件回调或者定时回调中调用某个函数时,this值指向的并非是想要的对象,此时需要将回调函数携程箭头函数,因为箭头函数中的this会保留定义该函数时的上下文

caller

es5中也会给函数对象arguments上添加一个属性caller,这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null

new.target

new.target负责检测函数是以什么方式调用的:

如果是正常调用,new.target的值为undefined,如果是使用new调用的,则new.target将引用被调用的构造函数

function king() {
    if(!new.target) {
        throw 'king must be instantiated using "new'
    }
    console.log('king instance use new')
}
new king();  //king instantiated using "new"
king();  //error:king must be instantiated using "new"

10.函数属性与方法

属性:

  • length:保存函数定义的命名参数的个数
function say(name) {
    console.log(name);
}
console.log(say.length);
  • prototype:保存引用类型所有实例方法(toString()等方法都保存在上面,以实现实例共享)

方法:

  • apply():会以指定的this值来调用函数,设置调用函数时函数体内的this对象的值,两个参数:函数内this的值和一个参数数组(可以是array和arguments对象)
function add(num1,num2) {
    return num1+num2;
}
function add1(num1,num2) {
    return add.apply(this,arguments);
}
console.log(add1(10,10));  //20
  • call():会以指定的this值来调用函数,设置调用函数Hi函数体内的this值,两个参数:函数内this的值和函数的参数 (需要一个一个传递)
function add(num1,num2) {
    return num1+num2;
}
function add1(num1,num2) {
    return add.call(this,num1,num2);
}
console.log(add1(10,10));  //20

apply()和call()最主要的作用是控制函数调用上下文即函数体内this值的能力

window.color = "red";
let o = {
    color:"blue"
};
function say() {
    console.log(this.color);
}
say();  //red
say.call(this);  //red
say.call(window);  //red
say.call(o);  //blue
  • bind():创建一个新的函数实例,其this值会被绑定到传给bind()的对象:
window.color = "red";
var o = {
    color:"blue"
};
function say() {
    console.log(this.color);
}
let objSay = say.bind(o);
objSay();  //blue

11.函数表达式

定义函数有两种方法:函数表达式和声明函数

函数表达式:需要先赋值在使用,没有函数变量提升

let say = function(name) {
    console.log(name);
}

这样创建的函数交匿名函数(兰姆达函数),因为function关键字后面没有标识符

使用场景:

let say;
if(condition) {
    say = function() {
        console.log("hi");
    }
}else{
    say = function() {
        console.log("he");
    }
}

12.递归

  • 递归函数通常的形式是一个函数通过名称调用自己:
function fac(num) {
    if(num<=1) {
        return 1;
    }else {
        return num*fac(num-1);
    }
}
  • 但是将函数赋值给其他变量的时候就会出错,解决方案是再写递归函数的时候使用arguments.callee( 指向正在执行的函数的指针)(严格模式下不能使用)
function fac(num) {
    if(num<=1) {
        return 1;
    }else {
        return num*arguments.callee(num-1);
    }
}
  • 在严格模式下可以使用命名函数表达式
const fac = (function f(num) {
    if(num<=1) {
        return 1;
    }else{
        return num*f(num-1);
    }
});

13.尾调用优化

es6新增了一项内存管理优化机制,让js引擎在满足条件时可以重用栈帧,适合用在尾调用,即外部函数的返回值是一个内部函数的返回值:

function out() {
    return in();  //尾调用
}

优化之前的机制:

  • 执行到out函数体,第一个栈帧被推到栈上
  • 执行out函数体,到return语句,计算返回值必须先计算in
  • 执行到in函数体,第二个栈帧被推到栈上
  • 执行in函数体,计算其返回值
  • 将返回值返回out,然后out再返回值
  • 将栈帧弹出栈外

es6优化之后:

  • 执行到out函数体 ,第一个栈帧被推到栈上
  • 执行out函数体,到达return语句,求值返回语句,必须先求值in
  • 引擎发现吧第一个栈帧弹出栈外也没问题,因为in的返回值也是out的返回值
  • 弹出out的栈帧
  • 执行到in函数体,栈帧被推到栈上
  • 执行in函数体,计算其返回值
  • 将in的栈帧弹出栈外

尾调用优化的条件

  • 代码在严格模式下执行
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自有变量的闭包

例如:

function fib(n) {
    if(n<2) {
        return n;
    }
    return fib(n-1)+fib(n-2);
}
//优化:
"use strict";
function fib(n) {
    return fibimp(0,1,n);
}
function fibimp(a,b,n) {
    if(n == 0){
        return a;
    }
    return fibimp(b,a+b,n-1);
}

14.闭包

闭包:引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现

正常来说,外部是无法访问函数内部的变量的,如果想要实现就需要使用闭包

function f1(){
  var n=999;
  function f2(){
    alert(n); // 999
  }

}

在上述代码中,f1中的变量对f2都是可见的,但是反过来不可以,即f1不能访问f2的变量,这是js独有的链式作用域结构:子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

所以,如果想要在外部访问f1的变量,可以再f2中将f1的变量return返回出去

function f1() {
    var n = 999;
    function f2() {
        alert(n);
    }
    return f2;
}
var result = f1();  //取得f1的返回值
result();  //执行f2

这里面的f2就是闭包

闭包的用途:

  • 可以读取函数内部的变量
  • 让这些变量的值始终保持在内存中
function f1(){
    var n=999;
    nAdd=function(){n+=1}
    
    function f2(){
      alert(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也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数,而这个匿名函数本身也是一个闭包,可以在函数外部对函数内部的局部变量进行操作。

关于this对象

  • 如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文
  • 如果在全局函数中调用,非严格模式下等于window
  • 如果作为某个对象的方法调用,则this等于这个对象
  • 匿名函数在这种情况下不会绑定到某个对象,这就意味着this会指向window
window.identity = 'the window';
let object = {
    identity:'my object',
    getIdentity() {
        return function() {
            return this.identity;
        };
    }
};
//调用getidentity返回的匿名函数
console.log(object.getIdentity()());  //;the window'

上述最后一条关于匿名函数:在getIdentity函数中,返回的是匿名函数,所以这个匿名函数中对应的this指向的是window,所以对应的是'the window'这个值

如果想要在内部函数访问外部函数的值:

window.identity = 'the window';
let object = {
    identity:'my object',
    getIdentity() {
        let that = this;
        return function() {
            return that.identity;
        };
    }
};
console.log(object.getIdentity()());  //my object

这里先在object对象中将this的值赋值给that,这样在返回的匿名函数中使用that的值去访问外部函数(对象)object的对象的值'my object'

问题:

闭包在使用的时候,会一直保持着对函数的引用,所以并不会被内存回收机制给回收,应该尽量减少使用闭包

15.立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE)

与函数声明类似,又被包含在括号中,所以会被解释为函数表达式

(function () {
    //块级作用域
})();

使用IIFE可以模拟块级作用域:

(function() {
    for(var i = 0;i<count;i++) {
        console.log(i);
    }
})();  //这个函数会被立即调用

es6出现了let,可以轻松实现块级作用域:

{
    let i;
    for(i = 0;i<count;i++) {
        console.log(i);
    }
}

for(let i = 0;i<count;i++) {
    console.log(i);
}

let作用域为块级作用域

16.私有变量

js没有私有成员的概念,但是有私有变量的概念

任何定义在函数或块中的变量,都可以认为是私有的,私有变量包括函数参数,局部变量,函数内部定义的其他函数

//这段代码有三个私有变量,num1,num2,sum
function(num1,num2) {
    let sum = num1+num2;
    return sum;
}

特权方法:能够访问函数私有变量的公有方法有两种方式创建特权方法:

  • 构造函数中实现:
function MyObject() {
    //私有变量和私有函数
    let private = 10;
    function privateFunction() {
        return false;
    }
    //特权方法
    this.publicMethod = function() {
        private++;
        return privateFunction();
    }
}

吧所有私有变量和私有函数都定义在构造函数中,然后创建一个能够访问到这些私有成员的特权方法。

定义在构造函数中的特权方法是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力

  • 使用私有作用域定义私有变量和函数:
(function() {
    //私有变量和私有函数
    let private = 10;
    function privateFunction() {
        return false;
    }
    //构造函数
    MyObject = function() {};
    //公有和特权方法
    MyObject.prototype.publicMethod = function() {
        private++;
        return privateFunction();
    };
})();

因为不使用关键字声明的变量会创建在全局作用域中,所以 Object变成了全局变量,可以再这个私有作用域被外部访问

而且这种方法定义的私有变量和私有函数是有实力共享的

模块模式:

在一个单例对象实现了相同的隔离和封装,单例对象是只有一个实例的对象,一般使用对象字面量来创建单例对象

let sing = {
    name:value,
    method() {
        //方法的代码
    }
};

模块模式是在单例对象基础上拓展,使其通过作用域链来关联私有变量和特权方法,模板:

let sing = function() {
    //私有变量和私有函数
    let privateVar = 10;
    function privateFunction() {
        return false;
    }
    //特权/共有方法和属性
    return {
        publicPrototype:true,
        publicMethod() {
            privateVar++;
            return privateFunction;
        }
    };
}();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值