函数概念
- 函数:function,又被称为子程序,面向对象时称之为实现某些功能的方法(method),例如:console.log(),这里的log()就是console这个对象里面所包含的其中一个方法,这个方法的功能便是函数的log()函数的功能
- 本质:将一段代码予以封装,以便于以后重用(反复使用)
- 对于 JavaScript 来说,函数就是把任意一段代码放在一个盒子里面,当想要让这段代码执行的时候,就直接执行这个盒子里面的代码就可以了
函数声明
声明式 - 命名函数
- 使用 function 关键字声明一个函数
- 声明式,可以先调用后定义
- 语法
function fn(){
//code......
}
赋值式 - 匿名函数
- 也称为“函数表达式”创建方式,这种创建方式是匿名函数的创建方法,即使用 var 定义一个变量,把一个函数当作值直接赋值给这个变量
- 赋值式,不能先调用后定义
- 语法
var fn = function(){
//code......
}
//不需要再 function 后面书写函数的名字了,因为在前面已经有了
声明式VS赋值式
test001() //正常
// test002() //报错,赋值式不能先调用后声明
function test001(){
console.log("test001")
}
var test002 = function (){
console.log("test002")
}
test001() //正常
test002() //正常
自执行
- 自执行函数是把创建函数和执行函数放在一起完成,具体做法是通过两个小括号实现
- 第一个小括号:一是存放函数,二是为了解决语法报错问题,而除了使用小括号
()之外,还可以使用~+-!让语法正确 - 第二个小括号:执行函数,也是从这里传递实参
(function(...params){
console.log("()"+params)
})(11.12,12)
//()11.12,12
//----------------------------
~function(...params){
console.log("~"+params)
}(12,50)
//~12,50
//----------------------------
+function(){
console.log("+",arguments)
}()
//+ Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
//----------------------------
-function(...params){
console.log("-"+params)
}(0,1,2)
//-0,1,2
//----------------------------
!function(...params){
console.log("!",params)
}()
//! []
箭头函数 ES6 - Lambda 表达式
- 箭头函数:ES6中创建函数的新方式,箭头函数与普通函数只是形式不同,本质相同,类似于 Java 中的 Lambda 表达式。
- 基本语法:将
() => {}赋值给一个变量 - 省略小括号:只有一个参数
- 省略大括号:仅仅返回一个值,可以省略 return 和大括号
- 剩余运算符:没有内置arguments,需要使用剩余运算符
- this指向父级:普通函数 this 指向运行时所在的对象,箭头函数的 this 指向定义时所在的对象(父级作用域对象,在哪里出生就指向谁,可以解决计时器的this指向问题),因此不能当作构造函数使用
//基本语法
var variable = () => {
console.log("sonething...");
}
//省略小括号:如果箭头函数只传递一个参数,可以省略括号
var variable = x => {
console.log("x is " + x);
}
//省略大括号和return:只有一句代码(这句话就是return,并且大括号省略,return 就必须省略,否则报错)
var fn = x => x+1
//剩余/展开运算符:箭头函数是没有自带的arguments集合的,只能使用剩余运算符,并且就是数值,不用转换
var fn001 = () => console.log(arguments)
var fn002 = (...params) => console.log(params)
// fn001(10,20) //ReferenceError: arguments is not defined
fn002(10,20) //[10,20]
//this指向父级:解决计时器的this指向问题
//example 1
function normalFn() {
let count;
count = setInterval(function () {
clearInterval(count);
console.log("count - 计时器ID值 - " + count);
console.log(this);
}, 1000)
}
function normalFnThat(){
let count;
let that = this; //强制改变this指向
count = setInterval(function(){
clearInterval(count);
console.log("count - 计时器ID值 - " + count);
console.log(that);
},1000);
}
function narrowFn() {
let count;
count = setInterval(() => {
clearInterval(count);
console.log("count - 计时器ID值 - " + count);
console.log(this);
}, 1000)
}
let objA = {
skill: "as a coder A",
work: normalFn
};
let objB = {
skill: "as a coder B",
work: normalFnThat
}
let objC = {
skill: "as a coder C",
work: narrowFn
};
objA.work(); // 1 Window 对象
objB.work(); // 2 {skill: 'as a coder', work: ƒ} 对象
objC.work(); // 3 {skill: 'as a coder', work: ƒ} 对象
//example 2
<body>
<input id="tx_1" type="button" value="待确认No.1" />
<input id="tx_2" type="button" value="待确认No.2" />
<input id="tx_3" type="button" value="待确认No.3" />
</body>
<script>
var value = "window's value";
tx_1.onclick = function () {
let countId = setTimeout(function () {
clearTimeout(countId);
console.log(this.value); //window's value
}, 1000);
}
//解决办法 1 :使用that = this
tx_2.onclick = function () {
let that = this;
let countId = setTimeout(function () {
clearTimeout(countId);
console.log(that.value); //待确认No.2
}, 1000);
}
//解决办法 2 :使用箭头函数
tx_3.onclick = function () {
let countId = setTimeout(() => {
clearTimeout(countId);
console.log(this.value); //待确认No.3
}, 1000);
}
</script>
- 练习 - 将普通函数改写为箭头函数
/* 原函数 */
function fn(x){
return function(y){
return x+y;
}
}
/* 改写 - 箭头函数 */
var fnNarrow = x => y => x+y
console.log(fn(2))
console.log(fnNarrow(2))
/**
* 原函数输出:
* ƒ (y){
* return x+y;
* }
* 箭头函数输出:y => x+y
*/
函数参数
形参和实参
- 在定义函数和调用函数的时候都出现过
(),这个()就是用来放置参数的,参数主要分为 形参 和 实参 两种
//形参和实参
//声明式
function fn([形参]) {
//code...
}
fn([实参])
//赋值式
function fn([形参]){
//code...
}
fn([实参])
内置实参集合
- 函数内置实参集合(arguments):执行函数的时候一般会传递实参值,但是传递多少个实参不确定,但我们又需要接受函数实时接收到的实参信息。而在以前传递实参的方法中,是需要需要知道传递实参的数量和顺序的,比如要传递3个值,那么就需要设置3个形参,并且这3个值的顺序也是需要明确的,但如果遇到不知道会传来多少个实参,也不知道先传什么后传什么,我们就需要采用一定的方法来解决这个问题,因此,我们可以通过自带的函数内置实参集合来实现。函数内置实参集合不论是否传递以及传递多少实参,在改集合中包含了所有传递进来的信息,如果不传递那就是一个空集合。
function fn(){
console.log(arguments)
}
fn();
/**输出结果:
*Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
* callee: ƒ fn()
* length: 0 这里说明没有传递实参,arguments是空集合
* Symbol(Symbol.iterator): ƒ values()
* __proto__: Object
*/
fn(10);
/**
*Arguments [10, callee: ƒ, Symbol(Symbol.iterator): ƒ]
* 0: 10
* callee: ƒ fn()
* length: 1
*Symbol(Symbol.iterator): ƒ values()
*__proto__: Object
*/
fn(10,20,30);
/**
*Arguments(3) [10, 20, 30, callee: ƒ, Symbol(Symbol.iterator): ƒ]
* 0: 10
* 1: 20
* 2: 30
* callee: ƒ fn()
* length: 3 有length属性的不是数组就是类数组,从这里可以看出并不是Array类的实例,因此这里可知arguments集合是一个类数组
* Symbol(Symbol.iterator): ƒ values()
* __proto__: Object
*/
- 从上述实例,可知,函数就不用设定形参了,arguments 是传递进来的所有实参的集合。
- 如果注册了DOM事件,那么arguments将会包含有点击事件对象的参数
function de(x){
console.log(arguments);
}
de(1); //Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]callee: ƒ de()length: 0Symbol(Symbol.iterator): ƒ values()__proto__: Object
let dc = function dd(){
console.log(arguments);
}
btn.onclick = dc; //Arguments [MouseEvent(鼠标事件对象), callee: ƒ, Symbol(Symbol.iterator): ƒ]
//在闭包测试中同样遇到这种情况
function defendShake(){
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(()=>{
Array.from(arguments).forEach(item=>{
console.log(arguments)
//Arguments [MouseEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
})
},500);
}
}
function fn1(){console.log("123")};
function fn2(){console.log("456")};
btn.onclick = defendShake(fn1,fn2);
剩余运算符
- 剩余运算符:在ES6规则中,采用了
...[剩余运算符]来表示传递的实参集合,一般会与params结合书写...params(params也可以任意自定义,例如,写成argus、data、digits等等,相当于一个数组名)。之所以称作剩余运算符,是在某些时候,会先制定几个确定的形参来接受实参,如果有更多的实参传递进来,则放进与剩余运算符关联的params集合中,这样就保证了确定的实参和多余的实参都能够获取到。因此,如果形参变量没有定义,则所有传递的实参信息都存到这个集合中,同时要注意使用剩余运算符所的集合是一个“数组”,并不是“类数组”。
function fn01(...params){
console.log(params)
}
fn01() //[]
fn01(10) //[10]
//----------------------
function fn02(...data){
console.log(data)
}
fn02(10) //[10]
//----------------------
function fn03(x,...digits){
console.log(digits)
}
fn03(10,20,30) //[20,30]
实参判断
- 在实际项目中,一般会设置相应的函数对形参进行默认值处理,即当执行函数时,传入的实参值没有给定,而此时又没有给定的实参值,便会将其设置为 undefined,为了减少报错,就需要对参数进行判断处理
var fn = x => {
x === undefined ? console.error(`x is ${x}`) : console.log(`OK,${x}`)
}
fn() //x is undefined
fn("James") //OK,James
形参默认值 ES6
- 问题1 代码描述:函数实现功能,如果传入一个数字,就输出这个数字,否则输出函数默认指定的数字
//如果传入了数字就输出该数字,否则输出默认的 10
function checkNum(num){
num = num || 10;
console.log(num);
};
checkNum(5); //5
checkNum(0); //10 -> 原生函数对于默认值的bug
//ES6 形参默认值 改进
function checkNum(num = 10){
console.log(num);
};
checkNum(5); //5
checkNum(0); //0
- 问题2 对传入的实参进行判断,确实能够减少报错的情况,但实参依然还是undefined,因此,在ES6中有更好的解决办法,直接给形参设置默认值,当没有传入实参的时候,就能避免undefined的情况了
- ES6形参赋值默认值:(规则)设定形参不传递值,走等号后面的默认值,但是一旦传递值了,不论传递什么都不会走默认值。
var average = (x = 0,y = 0) => {
return (x+y)/2
}
console.log(average(20,10)) //15
回调函数
- 回调:把函数作为参数,传递给另外一个函数,作为参数的函数就被称为回调函数
function isPositive(element){
return element > 0
}
function filter(arr,isPositive){
for (let index = 0; index < arr.length; index++) {
if(isPositive(arr[index])){
console.log(`${arr[index]} is positive.`)
} // else none to do...
}
}
filter([10,-10,20],isPositive)
//10 is positive.
//20 is positive.
- 示例:Array对象的forEach()方法就用到了回调函数,该方法的作用是遍历数组的元素,并依次执行将该方法中的回调函数。下面通过重写该方法说明。
Array.prototype.forEach = function forEach(callback){
//遍历数组
for(let i=0;i<this.length;i++){
callback(this[i],i,this);
}
//每个元素依次执行回调函数
}
//回调函数
function callback(item, index, arr){
console.log(`[${arr}] : ${index} - ${item}`);
}
//测试
let arr = ["one",2,"3","four"];
//重写前:
arr.forEach(callback); //[one,2,3,four] : 0 - one [one,2,3,four] : 1 - 2 [one,2,3,four] : 2 - 3 [one,2,3,four] : 3 - four
//重写后:
arr.forEach(callback); //与重写前相同
预解析(变量声明提升)
- JavaScript 是一个解释型语言,就是在代码执行之前,先对代码进行通读和解释,然后再执行代码,即JavaScript代码在运行的时候,会经历两个环节 解释代码 和 执行代码
- 解释代码就是在所有代码执行之前进行整体地通读和解释,也称为 预解析(预解释),需要解释的内容有两个:声明式函数、var 关键字
- 注意:变量提升/预解析,只会在一个script中进行提升,即不能跨脚本提升
sum(18,90) //108 因为预解析,且预解析的是整个声明式的函数
// ave(18,90) //TypeError:ave is not a function 预解析的是变量ave,因为是赋值的可能是函数,但此时还没有赋值(赋值在执行代码阶段),所以结果就不是undefined,而是not a function
console.log(num) //undefined 预解析的是变量num,此时还未赋值
//---------------------------
function sum(x=0,y=0){ //声明式函数
console.log(x+y)
}
var ave = (x=0,y=0) => console.log((x+y)/2) //var关键字(赋值式函数、箭头函数)
var num = 90;
//---------------------------
sum(18,90) //108
ave(18,90) //54
console.log(num) //90
重名问题
变量名重复
//变量名重复
//1. 后者覆盖前者
var name = "James"
var name = "Charles"
console.log(name); //Charles
//--> TIPS:下一句以小括号开头,上一行没有终止符,console.log可能会报错:console.log(...) is not a function,因此需要在console.log()后面加上分号
//2. 就近原则
(function(name){
var name = "Hello"
console.log(name) //Hello
})("James")
函数名重复
taken() //taken002 --> 声明预解析(声明提升)后也是一样,后者覆盖前者
function taken(){ console.log("taken001") }
function taken(){ console.log("taken002") }
taken() //taken002 --> 后者覆盖前者
变量名与函数名重复
- 结论先行:变量名与函数名不能重复,因为从本质上来说,函数名是一种特殊的变量名
/**
* 预解析阶段(变量提升)
* var name;
* function name(){ console.log("Charles") }
* --> 在此阶段输出name:因为在变量提升之后的顺序中,函数的位置在变量位置后面,因此此时的name是输出函数name()
* --> 在此阶段调用name(),便是执行函数,而不是调用变量
*/
console.log(name) //ƒ name(){ console.log( "Charles") } --> 声明预解析(变量提升),函数name()提升之后的位置在变量name的位置后面,因此,这里的name是函数name()
console.log(name()) //Charles 和 undefined --> 由于这里的name是函数name(),因此执行name()便能够得到函数name()的输出值
//-----------------在前面调用(预解析/变量提升)----------------//
var name = "James"
function nam(){ console.log( "Charles") }
//-----------------在后面调用(赋值阶段)----------------//
/**
* 赋值阶段
* name = "James"
* --> 在此阶段输出name: 因为函数已经在预解析阶段提升了,从而赋值阶段便只有变量被赋值,因此,输出就是变量name的值
* --> 输出name就是变量name的值,执行name()相当于是把变量name作为函数在执行,则会报错
*/
console.log(name) //James
console.log(name()) //Uncaught TypeError: name is not a function --> 说明这里的name并不是函数name(),而是变量name,所以,在开发中要注意变量尽量不与函数名相同,避免报错
作用域
- 基本概念:一个变量可以生效的范围 --> 变量不是在所有地方都可以使用,而可以使用的范围就是作用域
静态 VS 动态
- 静态作用域/词法作用域(static scope/lexical scope):一种根据语言文本的位置确定变量引用的规则,其作用域是定义函数的时候就已经决定了,即看的是书写位置,而不是看调用位置,JavaScript 就是静态作用域
- 动态作用域:不关心函数和作用域是如何声明以及在何处声明的,它们只关心在哪里调用,就是看调用位置,书写位置不重要
var lexicalValue = 100
function foo(){
console.log(lexicalValue);
}
function tool(){
var lexicalValue = 200
foo()
}
tool()
//静态作用域 -> 100 (JavaScript)
/**
* 原理:静态作用域看定义位置
* foo() 与 tool() 一样,定义的位置都是全局的下一级,
* 此时,执行 tool() 便会执行 内部调用的 foo() ,
* 而此时的 foo() 定义位置并不在 tool() 函数内部,
* 那么 foo() 中的 lexicalValue 变量就要向上级查找,
* 发现在全局作用域中有声明的 lexicalValue 变量,便直接拿来输出
*/
//动态作用域 -> 200
/**
* 原理:动态作用域看调用位置
* 不管 foo() 的定义位置,此时 foo() 是在 tool() 函数中调用的,
* 其上级作用域则属于 tool(),那么 foo() 在向上级查找 lexicalValue 时,
* 就是在 tool() 函数中的定义的 lexicalValue
*/
全局作用域
- 全局作用域是最大的作用域,即在全局作用域中定义的变量可以在任何地方使用,包括可以跨script使用
- JavaScript 所有数据变量都属于 window 对象,变量在函数外定义,即为全局变量,因此页面开启,浏览器会自动生成一个全局作用域 window,这个作用域直到页面关闭前都会一直存在
局部作用域
- 局部作用域是在全局作用域下面开辟出来的相对小一些的作用域,在局部作用域中定义的变量只能在这个局部作用域内部使用
- JavaScript 中只有函数能够生成一个局部作用域,即变量在函数内声明,则变量为局部作用域,包括函数形参
function myFunction() {
var carName = "Volvo"; // 局部变量: carName
}
/*---------------------*/
//形参是局部变量
function fn(x){
console.log(x);
}
fn(1) //1
console.log(x); //Uncaught ReferenceError: x is not defined
//将 x 不设置为形参,而是在函数体中直接赋值,则会向上级查找,如果外层都没有声明x,会定义为全局变量
function fun(){
x = 1
console.log(x)
}
fun() //1
console.log(x); //1
变量(作用域)访问规则
- 访问:获取变量值的行为
- 规则:
- 顺序:首先,在自己的作用域内部查找,如果有,就直接拿来使用;其次,如果没有找到,就去上一级作用域查找,如果有,就拿来使用;再次,如果还是未找到,就继续去上一级作用域查找,以此类推;最后,如果到全局作用域也没有这个变量,就报错 is not defined
- 注意:变量访问只能向上级作用域访问,不能向下访问
var num1 = 100
var num2Fn = () => {
var num2 = 200
var num3Fn = () => {
var num3 = 300
//从局部作用域开始查找
console.log(num1); //100
console.log(num2); //200
console.log(num3); //300
}
num3Fn()
}
num2Fn()
//直接从全局作用域开始查找
console.log(num1); //100 全局作用域
// console.log(num2); //num2 is not defined 变量访问规则,只能向上查找
// console.log(num3); //num3 is not defined 变量访问规则,只能向上查找
变量(作用域)赋值规则
- 赋值前提:要给变量赋值,就需要先找到这个变量
- 赋值规则:
- 首先,在自己作用域内部查找,有就直接赋值,其次,如果没有,就在上一级作用域查找,有就赋值,以此类推,如果一直找到全局作用域都没有进行过声明,那么就把这个变量定义为全局变量,再进行赋值
- 作用域为全局的变量可以作为window对象的属性进行调用、修改和删除,但一种特殊情况只能调用、修改,但不能删除:
- 如果是在全局有明确声明的(例如关键词var),window只能调用,不能删除
function name(){
name = "James"
var age
function age(){
age = 25
return age
}
console.log(name,age());
}
name() //James 25
console.log(name); //James
console.log(age); //Uncaught ReferenceError: age is not defined
/**变量赋值规则
* 1. name() 调用后,要给name和age分别赋值James和25
* 2. 那么name和age就会先在自己的作用域内部查找是否有name和age变量
* 3. 可见,name和age在自己的作用域内都没有发现已经声明的变量
* 4. name的上一级作用域是全局作用域,age的上一级作用域是name()函数
* 5. 此时,对于name而言,仍然没有已经声明的变量,因此,name被定义为全局变量
* 6. 同时,对于age而言,在name()函数作用域内找到了声明,因此调用name()函数才可访问
* 7. 注意:赋值的时候查找变量是就近原则的,因此要注意上一级相同变量名的变量值被覆盖
*/
//通过关键词明确声明变量
var host = "James"
//未通过关键字声明变量,而是通过作用域赋值规则将其创建为全局变量
range = 26
~function college(){
collegeSchool = "Peking University"
}()
//window 调用
console.log(window.host); //James
console.log(window.range); //26
console.log(window.collegeSchool); //Peking University
//window 配置-修改
window.host = "Char"
window.range = 30
window.collegeSchool = "Qinghua"
console.log(window.host); //Char
console.log(window.range); //30
console.log(window.collegeSchool); //Qinghua
//window 配置-删除
delete host
delete range
delete collegeSchool
console.log(window.host); //Char --> 说明未被删除
console.log(window.range); //undefined --> 被删除
console.log(window.collegeSchool); //undefined --> 被删除
变量生命周期
- JavaScript 变量的生命期从它们被声明的时间开始
- 局部变量:会在函数运行以后被删除
- 全局变量:会在页面关闭后被删除
闭包
- 闭包(closure):(狭义)就是 函数套函数,内部函数使用了外部函数定义的局部变量
- 解决问题:变量放在全局不安全,容易被篡改;也可以让临时变量永驻内存
- 弊端:性能问题,内存泄漏/溢出(如果在调用一个闭包赋值给全局变量,那么就会有一个作用域释放不掉,如果使用的闭包过多,就会导致内存溢出,因为在全局声明的变量直到程序结束才会被释放)
广义的闭包
var number = 0; //声明在函数外部的变量
function count(){
return number++; //在函数中使用外部声明的变量
}
狭义的闭包
function getCounts(){
//变量声明在另一个函数内部
var number = 0;
//函数表达式作为一个方法返回,这个返回的方法是还没有执行的,
//由此,number的声明周期就被延长了,
//因为,如果count()函数中没有使用number,那么,
//number 就会在getCounts()被调用结束后被销毁(出栈)
//但是,getCounts()执行完成后,返回的是一个函数(还未被调用),
//那么,number因为count()只是被返回,而没有被调用,
//所以,number的声明周期依然还在
return function count(){
return ++number;
}
}
console.log(getCounts());
//输出:
// ƒ count(){
// return number++;
// }
console.log(getCounts()()); //作为返回值的函数,采用自调用方式
//输出:
//1
console.log(number()) //报错 - Uncaught ReferenceError: number is not defined
//变量因为作用域,不容易被全局篡改 function getCounts(){
//变量声明在另一个函数内部
var number = 0;
//函数表达式作为一个方法返回,这个返回的方法是还没有执行的,
//由此,number的声明周期就被延长了,
//因为,如果count()函数中没有使用number,那么,
//number 就会在getCounts()被调用结束后被销毁(出栈)
//但是,getCounts()执行完成后,返回的是一个函数(还未被调用),
//那么,number因为count()只是被返回,而没有被调用,
//所以,number的声明周期依然还在
return function count(){
return ++number;
}
}
console.log(getCounts());
//输出:
// ƒ count(){
// return number++;
// }
console.log(getCounts()()); //作为返回值的函数,采用自调用方式
//输出:
//1
console.log(number()) //报错 - Uncaught ReferenceError: number is not defined
//变量因为作用域,不容易被全局篡改
柯里化函数
- 柯里化(Currying):是把接受多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且接受余下的参数且返回最终结果的新函数的技术。
- 核心思想:是一种预先存储的函数思想,首先把一些信息存储到某一个不被释放的上下文中,而后基于所用域链的机制,让其"下级上下文"访问到存储的信息值。
/**举例说明 - 函数柯里化:
* let total = fun(10,20)(30,40);
* console.log(total);
这里,执行fun是为了把一些信息事先存储起来(例如,10、20),
* 然后执行函数返回的小函数,这个过程中就传递另外的一些值(例如,30、40) ,
* 同时,小函数执行的时候,还会拿到之前存储的值(例如,10、20),
* 最后获取所有数值的和*/
function funDeclare() {
let arr, result;
//outerArgs: 存储的是预先存储的值(例如,预先存储10、20)
let outerArgs = Array.from(arguments);
return function proxy() {
//innerArgs: 小函数执行传递进来的信息值(例如,执行小函数时,传递的30、40)
let innerArgs = Array.from(arguments);
arr = outerArgs.concat(innerArgs);
/**Array.reduce()方法 -- 执行过程:
* 第一轮:preValue ->10 curValue ->20 return ->30
* 第二轮:preValue ->30 curValue ->30 return ->60
* 第三轮:preValue ->60 curValue ->40 return ->100
* 此时数组遍历完,最后一轮返回的值,作为reduce最终的返回值即可 */
arr.reduce((preValue, curValue) => {
return result = preValue + curValue;
});
return result;
}
}
//将上述声明式函数简化为箭头函数
let funNarrow = (...outerArgs) => (...innerArgs) => outerArgs.concat(innerArgs).reduce((pre,cur)=>pre+cur);
console.log(funDeclare(10, 20)(30, 40))
console.log(funNarrow(10, 20)(30, 40))
compose 组合函数
- 核心思想:compose 是基于柯里化函数基础上的一种特殊应用场景,在函数式编程中起到非常大的作用,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果。例如:现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,而如果每次调用时,都要操作两次,就会显得重复;因此,将这两个函数像管道一样组合起来,自动依次调用,就构成了组合函数(Compose Function)。
// ===========================================
/**举例说明(组合函数):
* const adv = a => a+1;
* const mis = m => m*3;
* const don = d => d/2;
* don(mis(adv(2))); //4.5
* 这样的写法明显太冗余了,因此,可以构建一个compose函数,
* 让它接受任意多个函数作为参数(而这些回调函数都只接受一个参数),
* 然后compose返回的也是一个函数。达到以下的效果:
* const compose = opera(adv, mis, don);
* operator(0); //相当于 don(mis(adv(0)))
* operator(2); //相当于 don(mis(adv(2)))
* 由此可见,compose 把类似于 fn1(fn2(fn3(x))) 的这种写法简化成了 compose(fn1,fn2,fn3)(x)
* 要实现这一功能,首先就需要将fn1、fn2、fn3等回调函数,事先存储起来,确定好执行顺序(这个过程就是 柯里化),
* 其过程就是,分别把某个函数执行的返回值作为“实参”传递给下一个函数,最后实现函数的嵌套
* 而compose组合函数本质上,就是把这个过程扁平化了
*/
const adv = a => a+1;
const mis = m => m*3;
const don = d => d/2;
const compose = (...params) => {
//...params: 预先存储的就是未来要执行的函数集合
// -> 柯里化要做好预先存储,就是要明确函数执行顺序。
// 例如,这里就是 [adv,mis,don] 的顺序,当要执行operatCompose([实参])的时候,
// 会按照事先先存储的函数执行顺序,以此执行
//x: 作为operaCompose函数的形参,是参与回调函数计算的最原始参数
return x => {
//遍历执行传进来的回调函数 -> reduceRight()让数组元素从右向左遍历
return params.reduceRight((pre,cur)=>{
return cur(pre);
//第一轮:pre -> 最右边的
},x)
}
}
//原函数调用:
console.log(don(mis(adv(2))));
//组合函数调用:
let operatCompose = compose(don,mis,adv);
console.log(operatCompose(2));
// ===========================================
/**组合函数改写练习:
* const fn1 = x => x+65;
* const fn2 = x => x*30;
* const fn3 = x => x/20;
*/
//原始函数:
const fn1 = x => x+65;
const fn2 = x => x*30;
const fn3 = x => x/20;
//原始函数调用:
console.log(fn3(fn2(fn1(10)))); //112.5
//compose组合函数:
const composeFn = (...params) => {
return x =>{
//扁平化柯里化
return params.reduceRight((result,item)=>{
return item(result);
},x);
}
}
let operatComposeFn = composeFn(fn3,fn2,fn1);
console.log(operatComposeFn(10)); //112.5
示例 - 标记列表索引
<ul id="ul">
<li><button>1</button></li>
<li><button>2</button></li>
<li><button>3</button></li>
<li><button>4</button></li>
</ul>
<script type="text/javascript">
var lis = ul.querySelectorAll("li");
for(var i = 0;i < lis.length;i++){
//通过闭包,让 lis[i] 绑定的事件函数是相互独立的,
//闭包的返回值不会被释放,因此每次循环所绑定的事件函数就被保留下来了
//为了让事件绑定闭包函数,需要让外层函数自执行,同时为了获取到下标,需要传入下标的值
//注意 -> 自执行性函数使用 ! + - ~ 四种符号,不能有效传递回调函数给事件,因此建议使用 ()
lis[i].onclick = (function(index){
return function(){
console.log(index);
};
})(i);
};
</script>
示例 - 优化 fetch
//不使用闭包 - fetch请求
fetch("http://localhost:5500/aaa").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/bbb").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/ccc").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/ddd").then(res => res.json()).then(res => console.log(res));
//使用闭包 - fetch请求 -> 函数柯里化的体现
function fetchFunction(url) {
return function (path) {
return fetch(url + path);
}
}
let fetchFn = fetchFunction("http://localhost:5500/");
fetchFn("aaa").then(res => res.json).then(res => console.log(res))
fetchFn("bbb").then(res => res.json).then(res => console.log(res))
fetchFn("ccc").then(res => res.json).then(res => console.log(res))
fetchFn("ddd").then(res => res.json).then(res => console.log(res))
fetchFn = null; //闭包需要释放内存,否则累积过多会导致内存溢出
示例 - 优化 jsonp
<input type="text" id="inp" />
<ul id="lists"></ul>
<script type="text/javascript">
inp.oninput = (function () {
var timer = null;
return function () {
if (timer) {
clearTimeout(timer);
} //else none
timer = setTimeout(() => {
var inpContent = this.value;
if (inpContent < 1) {
lists.innerHTML = "";
return;
}
var jsonpScript = document.createElement("script");
jsonpScript.src = `https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=36556,37495,37841,37765,37866,37800,37760,37850,26350,37790&wd=${inpContent}&req=2&csor=14&pwd=1&cb=jQuery110206459462907160005_1669888754740&_=1669888754754`;
document.body.appendChild(jsonpScript);
jsonpScript.remove();
}, 500);
}
})();
function jQuery110206459462907160005_1669888754740(obj) {
lists.innerHTML = obj.g.map(item => `<li>${item.q}</li>`).join("");
}
</script>
防御式和截断式
- 防御式编程:(防错误)在函数主体代码执行之前首先检查传入的参数是否适合该函数
//示例 - 防御式编程
function isPositive(num){
if (isNaN(num)) {
return "非数字,无法判断";
} else {
return num > 0
}
}
console.log(isPositive(90)) //true
console.log(isPositive("90")) //true
console.log(isPositive("ninety")) //非数字,无法判断
- 截断式编程:(简单化)将一个复杂问题中较为简单的部分剥离出来,优先处理
//示例 - 截断式编程
function login(name,psw){
if (name == null || psw == null) {
return "登录请输入"
} //else to do none ...
//other function...
}
console.log(login()); //登录请输入
断点调试
函数如果需要断点调式,有两个关键功能键:
- F11 进入函数内部
- Shift 跳出当前函数
递归
- 递归调用:在方法体中调用方法自己
- 优点:精简代码,例如,二分查找
- 缺点:占用栈资源,导致运行效率从低,可能造成堆栈溢出(stack overflow)
function getFibonacci(a, b){
// 递归函数一定要有终止条件
if(b>10){
return a+b
}else{
getFibonacci(b,a+b) //Fibonacci数列的核心 --> 递归调用(调用自身)
}
} //在函数执行结束时,a,b 都逐渐恢复到初始参数的值
console.log(getFibonacci(0,1)) //undefined --> 递归最终返回值是最初那一层的返回值,因此递归如果要返回值,最好是用第三个变量承接
//正确写法
var sum; //接受递归返回值
function getFibonacci(a, b){
// 递归函数一定要有终止条件
if(b>10){
sum = a+b
}else{
getFibonacci(b,a+b) //Fibonacci数列的核心 --> 递归调用(调用自身)
}
return sum;
} //在函数执行结束时,a,b 都逐渐恢复到初始参数的值,因为递归调用是压栈,结束函数,要等全部出栈才会结束
console.log(getFibonacci(0,1)) //21
示例练习
任意数求和
// version 1.0 声明式函数
function sum(){
var result = 0;
for(i=0;i<arguments.length;i++){
result += Number(arguments[i])
}
isNaN(result)?console.log("Error"):console.log(result);
}
sum() //0
sum(10,10) //20
sum(10,"10") //20
sum(10,"十") //Error
sum(10,10.2,-45) //-24.8
// version 2.0 箭头函数
var sum = (...params) => {
var result = 0;
var i = 0;
for(;i<params.length;){
result += Number(params[i])
i++
}
isNaN(result)?console.log("Error"):console.log(result)
}
sum() //0
sum(10,10) //20
sum(10,"10") //20
sum(10,"十") //Error
sum("10",10.2,-45) //-24.8
// version 3.0 箭头函数+forEach方法
var sum = (...params) => {
result = 0
params.forEach(function(value){
result += Number(value)
});
isNaN(result)?console.log("error"):console.log(result)
}
sum(10,20) //30
sum(10,-20) //-10
sum(10,-20,"-82") //-92
sum(10,"十",20) //error
//version4.0 优化forEach方法中的函数为箭头函数
var sum = (...params) => {
result = 0;
params.forEach(value => {
result += Number(value)
})
isNaN(result)?console.log("error"):console.log(result)
}
sum(10,20) //30
sum(1.20,-20) //18.8
sum(10,"-82",-20) //-92
sum(10,"十",20.3) //error
函数调用
var i=1;
function mySecFunciton(){
console.log(i);
}
function myFunction(){
var i = 2;
mySecFunciton();
}
myFunction()
// 1
var i=1;
function myFunction(){
var i = 2;
console.log(i);
}
myFunction()
//2
var i=1;
function mySecFunciton(m){
console.log(m);
}
function myFunction(){
var i = 2;
mySecFunciton(i);
}
myFunction()
//2
var i=1;
function mySecFunciton(m){
console.log(m);
}
function myFunction(){
var i = 2;
mySecFunciton(n); //此处是调用,因此n是实参
}
myFunction()
//报错 ReferenceError: n is not defined
function mySecFunciton(){
console.log(i);
}
function myFunction(){
var i = 2;
mySecFunciton();
}
var i=0;
var i=1;
myFunction()
//1