JS高级编程(二)-函数和闭包

摩森特沃 2021年03月08日 381次浏览

第一公民-函数

在 JavaScript 中,因为函数可以被当做一个普通的对象来看待,所以它是第一公民。具体表现为:函数可以被作为一个参数传给函数、可以作为函数的返回值、可以和普通对象一样有自己的键值对、以及可以被 push 进数组之中等等

函数的参数arguments

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组非常相似,但是它们却没有数组常见的方法属性,如forEach, reduce等,通常会把这样与数组类似的对象叫做类数组

遍历类数组的方式

  • 将数组的方法应用到类数组上,这时候就可以使用call和apply方法
function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}
  • 使用Array.from方法将类数组转化成数组
function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}
  • 使用展开运算符将类数组转化成数组
function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

定义函数的方式

字面量定义的方式

// 函数声明的方式
function add() {}
// 赋值表达式
var add = function(){};

构造函数定义的方式

var add = new Function('num1', 'num2', 'return num1 + num2;');

函数的直接调用和间接调用

直接调用

  • 通过命名函数进行调用
  • 通过匿名函数赋值变量进行调用
  • 使用自执行函数方式调用(详细内容可以参照下文中的立即执行函数(IIFE))

使用call和apply间接进行调用

  • 通过间接的方式调用方法
function add(num1, num2){
    return num1 + num2;
}
console.log(add.call(window, 1, 2));
console.log(add.applay(window, [1, 2]));
  • 借用其他对象的方法

通过改变this的指向进行方法的调用,即从拥有方法的对象借用方法,传入自己的参数进行调用

Array.prototype.join.call('abc', '|');
// 输出结果
// a|b|c

小结

  • 函数完整的调用方法是使用call方法,包括test.call(context, name)和obj.greet.call(context, name),这里的context就是函数调用时的上下文,也就是this,只不过这个this是可以通过call方法来修改的

函数内部调用函数(递归)

使用arguments.callee指代当前函数(严格模式下不允许)

functon jiecheng(num){
    if(num <= 1) return 1;
    return num * arguments.callee(num - 1);
}

同时使用命名函数和匿名函数

var jiecheng = function fn(num) {
    if (num <= 1) return 1;
    return num * fn(num - 1); 
// 采用此种写法,fn只能在函数内部使用,且jiecheng作为函数名不能在内部使用
}

立即执行函数

相关概念

  • 函数声明:function fname(){...} 使用function关键字声明一个函数,再指定一个函数名
  • 函数表达式:var fname=function(){...} 使用function关键字声明一个函数,但未给函数命名,最后将匿名函数赋予给一个变量
  • 匿名函数:function(){} 使用function关键字声明一个函数,但未给函数命名
    • 匿名函数也属于函数表达式
    • 匿名函数作用很多,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序等
  • 关于函数声明和函数表达式的区别
    • 函数声明具有函数声明提升的特性,函数表达式不具备这点,它需要被js代码解析到当前这行时才可以调用
    • 函数表达式后边加 () 立即调用该函数,函数声明不可以,它只能以fname()调用
fName();
function fName(){...}// 正确,函数声明提升,所以 fName()可以写在函数声明之前

fName();
var fName=function(){...}// 错误,函数表达式不具备函数声明提升。

var fName=function(){...}();// 正确,函数表达式后边加()立即调用函数

function fName(){...}();// 错误,函数声明必须用fName()调用

function (){...}();// 匿名函数不可以这么调用,因为function(){...}被当做了声明,声明不可以直接()调用

立即执行函数

  • 传统的函数声明和调用
function foo(){...} // 这是定义,Declaration,只是让解释器知道其存在,不会运行

foo(); // 这是语句,解释器遇到语句会运行它
  • 相比之下立即执行函数((IIFE,即:Immediately Invoked Function Expression))的特点
  • 写法简单直接
  • 不会污染全局命名空间
  • 常用的自执行函数写法

通过把函数声明变成函数表达式的方式可以让函数变成立即执行函数,其实质是使代码不以function关键字开头,从而避免被解析器解析为函数定义,常用写法如下:

  • (函数本身)():(function func(){console.log("自执行函数")})()
  • (函数本身()):(function func(){console.log("自执行函数")}())

除以上写法外,还可以使用以下方式:

  • !+-~符号 + 函数本身():!function func(){console.log("自执行函数")}()
  • void + 函数本身():void function func(){console.log("自执行函数")}()

不管使用那种写法,最终结果等价于

var foo= function(){...};
foo();
  • 往立即执行函数中传入参数
// 传入全局对象,使得函数内部可以与全局对象进行交互
// 通常还会在立即执行函数中将变量或函数等设置为全局对象的属性和方法的做法以达到暴露内部属性和函数给外部使用的目的
void function(global) {
    console.log("a's value is: " + global.aa);  //可以获取全局对象中aa的值
}(this)
// 直接使用全局对象中的某个属性
var aa=10;
(function(a) {
    console.log("hello world" + a);
})(aa);

apply,call,bind

相同点

  • call()、apply()、bind()都是用来重定义this这个对象的
  • call、bind、apply这三个函数的第一个参数都是this的指向对象

不同点

  • call的参数是直接放进去的,多个参数全时用逗号分隔,直接放到后面
  • apply的所有参数都必须放在一个数组里面传进去
  • bind除了返回是函数以外,它的参数和call一样

示例代码

var obj = {
    x : 81
}
var myFun = function(y, z) {
    alert(this.x + y + z);
}

myFun(10, 9); // NaN
myFun.call(obj, 10, 9); // 100
myFun.apply(obj, [10, 9]); // 100
myFun.bind(obj)(10, 9); // 100

函数的形参和实参长度

function add(num1, num2) {
    if(arguments.length != add.length) {
	throw new Error("请传入" + add.length + 参数);
    }
    return num1 + num2;
}
// arguments.length表示实参的个数
// 函数名.length表示形参的个数

闭包

js作用域与闭包概念

JavaScript 的作用域是基于词法作用域的,词法作用域的内涵是:作用域在词法分析阶段就被确定了,也就是说,变量在代码中所处的位置 (而不是运行时所处的位置),决定了作用域

由于在 JavaScript 中,函数是第一公民,所以函数可以被当作一个普通的变量传递,所以函数在运行时可能会看起来已经脱离了原来的词法作用域。但是由于函数的作用域早就在词法分析时就确定了,所以函数无论在哪里执行,都会记住被定义时的作用域。这种现象就叫作闭包

闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时

function foo() {
  const a = 2;
  return function bar() {
    console.log(a)
  }
}

const func = foo()
func() 
// 打印出 2
// 当函数bar执行时,很明显其早已脱离了原来的作用域,但是其依然打印出变量a的值
// 这就说明它一直记住了它在被定义时的作用域

闭包的经典案例-函数柯里化

function carry(func) {
    return function carried(...args) {
        if(args.length >= func.length) {
	    // 当传入的参数大于或者等于func函数本身的参数长度时,直接调用func函数
	    // 此处的this表示的window对象
	    return func.apply(this, args);
	} else {
	    // 当传入的参数小于func函数本身的参数长度时,返回一个包装函数,此时传入的args参数将在函数内部驻留
	    // 包装函数执行时,继续返回一个柯里化的函数,并将参数进行拼接
	    // 最终拼接的函数长度等于func函数的参数长度时,即执行原方法
	    return function pass(...args1) {
		// 此处的this表示的window对象
		return carried.apply(this, args.concat(args1));
	    }
	}
    }
}

// 测试示例
function sum(a, b, c) {
    return a + b + c;
}
let carriedSum = carry(sum);
alert(carriedSum(1, 2, 3)); // 输出6
alert(carriedSum(1)(2, 3)); // 输出6
alert(carriedSum(1)(2)(3)); // 输出6