# function

  • 问题:为什么写IIFE时通常在前面加个分号
;(function(){
    var a;
    //code
}())

括号有个缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末的函数调用,产生完全不符合预期,并且难以调试的行为,加号等运算符也有类似的问题。

void function(){
    var a;
    //code
}();

这有效避免了语法问题,同时,语义上void运算表示忽略后面表达式的值,变成undefined,也确实不关心IIFE的返回值,所以语义也更为合理。

# 函数function fun 和 fun=function(){}

其实他们的主要区别就是“函数声明的提前行为”.

  • 函数表达式:使用function声明函数,但未指定函数名,将匿名函数赋予一个变量;不能提升函数,只能提升变量fun=undefind
fun();       //报错
var fun=function(){
  alert("hello world!");
};
  • 函数声明:使用function声明函数,并指定函数名;会将整个函数提升到最上方(如果)
var fun=function(){
	console.log(1)
}
fun()//1
function fun(){
	console.log(2)
}
fun()//1
fun()//2 
var fun=function(){
	console.log(1)
}
function fun(){
	console.log(2)
}
fun()//1

函数表达式前使用了该函数会报错,而函数声明则不会!注意的是,如果上面代码有同名的函数或者变量(需要赋值),那么即使表达式写在下方,也会无效。

  • 函数声明如果有判断条件,不同情况执行不同的逻辑,则不适用,可以采取函数表达式的方式
// Never do this!
if (condition) {
  function sayHi() {
    console.log('Hi!');
  }
} else {
  function sayHi() {
    console.log('Yo!');
  }
}
// OK
let sayHi;
if(condition) {
  sayHi = function() {
    console.log("Hi!");
  };
} else {
  sayHi = function() {
    console.log("Yo!");
  };
}

区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

# 函数参数是值传递

简单类型值传递,对象类型,会共享原先的栈里面对象的信息,对象在堆里,如果直接改变对象的栈地址,那么就再也不会有关联

function setName(obj) {
    obj.name = "Nicholas";
    obj = new Object(); //改变obj的指向,此时obj指向一个新的内存地址,不再和person指向同一个
    obj.name = "Greg";
}

var person = new Object();
setName(person);  
console.log(person.name);  //"Nicholas"
let foo = 1
const bar = value => {
   value = 2
   console.log(value)
}
bar(foo) // 2
console.log(foo) // 1
let foo = {bar: 1}
const func = obj => {
   obj.bar = 2
   console.log(obj.bar)
}
func(foo)
console.log(foo)
//2、{bar: 2};
let foo = {bar: 1}
const func = obj => {
   obj = 2
   console.log(obj)
}
func(foo)
console.log(foo)
//2、{bar: 1}
  • 参数为基本类型时,函数体内复制了一份参数值,对于任何操作不会影响参数实际值
  • 函数参数是一个引用类型时,当在函数体内修改这个值的某个属性值时,将会对参数进行修改
  • 函数参数是一个引用类型时,如果我们直接修改了这个值的引用地址,则相当于函数体内新创建了一份引用,对于任何操作不会影响原参数实际值

# function 参数

默认参数,如果没有,可以用undefined代替

function fun(a,b=4,c){
	console.log(a+b+c)
}
fun(1,undefined,4)//9

# function的length

形参的个数指该函数有多少个必须要传入的参数

  • …args 不算;
  • 有默认值如a = 1 不算以及其后都不算
function fun1(a) { }
function fun2(a, b) { }
function fun3(a, b, c) { }
function fun4(a, b, c, d) { }
function fun5(...args) { }
function fun6(a = 1, b, c, d) { }
function fun7(t1,t2,a = 1, b, c, d) { }

console.log(fun1.length) // 1
console.log(fun2.length) // 2
console.log(fun3.length) // 3
console.log(fun4.length) // 4
console.log(fun5.length) // 0
console.log(fun6.length) // 0
console.log(fun7.length) // 2

# new Function

// 前面参数作为函数的入参,最后一个作为函数体
let sum = new Function('a','b','return a+b')
console.log(sum(3,5))

以上写法不推荐

# 函数名

函数名就是指向函数的指针,es6函数会暴露一个name只读属性

function sum(num1, num2) {
  return num1 + num2;
}
 
console.log(sum(10, 10));         // 20
                   
let anotherSum = sum;        
console.log(anotherSum(10, 10));  // 20
                   
sum = null;        
console.log(anotherSum(10, 10));  // 20
function foo() {}
let bar = function() {};
let baz = () => {};

console.log(foo.name);               // foo
console.log(bar.name);               // bar
console.log(baz.name);               // baz 
console.log((() => {}).name);        // (empty string)
console.log((new Function()).name);  // anonymous

# arguments

参数与arguments同步,但不是一个内存地址。arguments更改形参,在严格模式得出结果不一致。

"use strict"
function fun(a,b){
	arguments[1]=1000
	console.log(b)//1
	b=30
	console.log(arguments[1])//1000
	arguments[1]=100
	console.log(arguments[1])//100
	console.log(b)//30
}
fun(0,1)
function fun(a,b){
	arguments[1]=1000
	console.log(b)//1000
	b=30
	console.log(arguments[1])//30
	arguments[1]=100
	console.log(arguments[1])//100
	console.log(b)//100
}
fun(0,1)

如果一开始就没传参数b,那么修改arguments[1]不影响b的变化,因为arguments长度根据传入的参数决定的

function fun(a,b){
	console.log(arguments.length)//1
	arguments[1]=1000
	console.log(b)//undefined
	b=30
	console.log(arguments[1])//1000
	arguments[1]=100
	console.log(arguments[1])//100
	console.log(b)//30
	console.log(arguments.length)//1
}
fun(0)

# arguments转数组和slice实现原理

//利用数组slice方法
var newArr2 = Array.prototype.slice.call(arguments)
console.log(newArr2)

var newArr3 = [].slice.call(arguments)
console.log(newArr3)

//ES6的语法
var newArr4 = Array.from(arguments)
console.log(newArr4)
var newArr5 = [...arguments]
console.log(newArr5)
  • 模拟slice实现原理
Array.prototype.hyslice = function(start, end) {
  var arr = this
  start = start || 0
  end = end || arr.length
  var newArray = []
  for (var i = start; i < end; i++) {
    newArray.push(arr[i])
  }
  return newArray
}

箭头函数是不绑定arguments的,所以在箭头函数中使用arguments会去上层作用域查找

# arguments.callee

可以让函数逻辑与函数名解耦,arguments.callee在函数中指向factorial的引用,这样中途修改名称也无所谓

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

let s =factorial(10)
console.log(s)//3628800

严格模式不能使用

# 函数重载

js没有函数重载,不过可以通过传不同的参数依据个数模拟实现。

function doAdd() {
  if (arguments.length === 1) {
    console.log(arguments[0] + 10);
  } else if (arguments.length === 2) {
    console.log(arguments[0] + arguments[1]);
  }
}
                   
doAdd(10);        // 20
doAdd(30, 20);    // 50

# 函数参数扩展与收集

  • 参数扩展
let values = [1,2,3,4]

function countArguments() {
  console.log(arguments.length);
}

countArguments(-1, ...values);         // 5
countArguments(...values, 5);          // 5
countArguments(-1, ...values, 5);      // 6
countArguments(...values, ...[5,6,7]); // 7
  • 收集参数
function getSum(...values) {
  // Sequentially sum all elements in 'values'
  // Initial total = 0
  return values.reduce((x, y) => x + y, 0);
}

console.log(getSum(1,2,3));  // 6

# this

箭头函数与普通函数的this指向规则不一致

window.color = 'red';
let o = { 
  color: 'blue'
};
                   
function sayColor() {
  console.log(this.color);
}
                   
sayColor();     // 'red'
                   
o.sayColor = sayColor;
o.sayColor();   // 'blue'
window.color = 'red';
let o = { 
  color: 'blue'
};
                   
let sayColor = () => console.log(this.color);
                   
sayColor();     // 'red'
                   
o.sayColor = sayColor;
o.sayColor();   // 'red'

  • 赋值表达式的是函数本身,this值不在与任何对象绑定,所以返回“The Window”
window.identity = 'The Window';
let object = {
  identity: 'My Object',
  getIdentity () {
	return this.identity;
  }
};
console.log(object.getIdentity()) // 'My Object'
console.log((object.getIdentity = object.getIdentity)())// 'The Window'     

this

# caller

这个属性引用的是 调用当前函数的函数 ,如果是在全局作用域中调用则为null

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
  // console.log(arguments.callee.caller);
}
outer();

// ƒ outer() {
//   inner();
// }

严格模式不能使用

# function使用valueOf 和 toString

function fun(){
	console.log('1')
}
console.log(typeof fun.toString())//string
console.log(typeof fun.valueOf())//function

# 纯函数和副作用

  • 确定的输入,一定会产生确定的输出;
  • 函数在执行过程中,不能产生副作用;

react的函数组件,redux中的reducer就是纯函数。

# 副作用

在计算机科学中,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;

# 纯函数的优势

  • 可以安心的编写和安心的使用;
  • 写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;

对数组操作的两个函数:

  • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
  • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;
  • slice就是一个纯函数,不会修改传入的参数

# 函数柯里化

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数;这个过程就称之为柯里化

作用:

  • 让函数的职责单一,结构清晰
  • 复用部分逻辑
function add(x, y, z) {
  return x + y + z
}

var result = add(10, 20, 30)
console.log(result)

function sum1(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}

var result1 = sum1(10)(20)(30)
console.log(result1)

// 简化柯里化的代码
var sum2 = x => y => z => {
  return x + y + z
}

console.log(sum2(10)(20)(30))

var sum3 = x => y => z => x + y + z
console.log(sum3(10)(20)(30))

function makeAdder(count) {
  count = count * count

  return function(num) {
    return count + num
  }
}

// var result = makeAdder(5)(10)
// console.log(result)
var adder5 = makeAdder(5)
console.log(adder5(10))//35
console.log(adder5(14))//39

# 柯里化函数的自动实现

fn.length如果add1中参数有默认值或者...args,那么可能会出现错误结论

function add1(x, y, z) {
  console.log(this)
  return x + y + z
}

// 柯里化函数的实现hyCurrying
function hyCurrying(fn) {
  function curried(...args) {
    // 判断当前已经接收的参数的个数, 可以参数本身需要接受的参数是否已经一致了
    // 1.当已经传入的参数 大于等于 需要的参数时, 就执行函数
    if (args.length >= fn.length) {
      // fn(...args)
      // fn.call(this, ...args)
      // 让curried函数和fn函数的this一致
      return fn.apply(this, args)
    } else {
      // 没有达到个数时, 需要返回一个新的函数, 继续来接收的参数
      function curried2(...args2) {
        // 接收到参数后, 需要递归调用curried来检查函数的个数是否达到
        return curried.apply(this, args.concat(args2))
      }
      return curried2
    }
  }
  return curried
}


var curryAdd = hyCurrying(add1)


// console.log(curryAdd(10, 20, 30))
// console.log(curryAdd(10, 20)(30))
// console.log(curryAdd(10)(20)(30))

let obj={
	
}

console.log(curryAdd.call(obj,10, 20, 30))

# 柯里化实际应用(可以最大程度的重用函数)

function curry(fn) {
	  // curriedFn 为柯里化生产的新函数
	  // 为什么不使用匿名函数?因为如果传入参数 args.length 小于 fn 函数的形参个数 fn.length,需要重新递归
	  return function curriedFn(...args) {
	    if (args.length < fn.length){
	      return function() {
	        // 之前传入的参数都储存在 args 中
	        // 新函数参入参数在 arguments,因为 arguments 并非真正的数组需要 Array.from() 转换成数组
	        // 递归执行,重复之前的过程
	        return curriedFn(...args.concat(Array.from(arguments)))
	      }
	    }
	    return fn(...args)
	  }
	}
	 //进行柯里化
	 let _check = curry(checkByRegExp);
	 //生成工具函数,验证电话号码
	 let checkCellPhone = _check(/^1\d{10}$/);
	 //生成工具函数,验证邮箱
	 let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
	 
	 console.log(checkCellPhone('18642838455'))
	 console.log(checkCellPhone('18645'))
	
	 console.log(checkEmail('test@163.com'))
	 console.log(checkEmail('tes163.com'))
	
	 function checkByRegExp(regExp,string) {
	     return regExp.test(string);  
	 }
// 非柯里化版本
function checkByRegExp(regExp,string) {
    return regExp.test(string);  
}

checkByRegExp(/^1\d{10}$/, '15010001000'); // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱

# 组合函数

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式

  • 比如现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
  • 那么如果每次都需要进行两个函数的调用,操作上就会显得重复;
  • 是否可以将这两个函数组合起来,自动依次调用呢?
  • 这个过程就是对函数的组合,称之为 组合函数(Compose Function);
function double(num) {
  return num * 2
}
function square(num) {
  return num ** 2
}

var count = 10
var result = square(double(count))
console.log(result)

// 实现最简单的组合函数
function composeFn(m, n) {
  return function(count) {
    return n(m(count))
  }
}
var newFn = composeFn(double, square)
console.log(newFn(10))

# 高阶函数

高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

# compose函数和pipe函数

  • compose函数

compose函数可以将需要嵌套执行的函数平铺,嵌套执行就是一个函数的返回值将作为另一个函数的参数。一个简单的需求:给定一个输入值x,先给这个值加10,然后结果乘以10;这个需求很简单,直接一个计算函数就行:

const calculate = x => (x + 10) * 10;
let res = calculate(10);
console.log(res);    // 200

根据函数式编程,可以将复杂的几个步骤拆成几个简单的可复用的简单步骤,拆出了一个加法函数和一个乘法函数:

const add = x => x + 10;
const multiply = x => x * 10;

// 我们的计算改为两个函数的嵌套计算,add函数的返回值作为multiply函数的参数
let res = multiply(add(10));
console.log(res);    // 结果还是200

上面的计算方法就是函数的嵌套执行,compose的作用就是将嵌套执行的方法作为参数平铺,嵌套执行的时候,里面的方法也就是右边的方法最开始执行,然后往左边返回,我们的compose方法也是从右边的参数开始执行,所以目标就很明确了,需要一个像这样的compose方法:

// 参数从右往左执行,所以multiply在前,add在后
let res = compose(multiply, add)(10);
  • Array.prototype.reduce

数组的reduce方法可以实现一个累加效果,它接收两个参数,第一个是一个累加器方法,第二个是初始化值。累加器接收四个参数,第一个是上次的计算值,第二个是数组的当前值,主要用的就是这两个参数,后面两个参数不常用,他们是当前index和当前迭代的数组:

const arr = [[1, 2], [3, 4], [5, 6]];
// prevRes的初始值是传入的[],以后会是每次迭代计算后的值
const flatArr = arr.reduce((prevRes, item) => prevRes.concat(item), []);

console.log(flatArr); // [1, 2, 3, 4, 5, 6]
  • Array.prototype.reduceRight

Array.prototype.reduce会从左往右进行迭代,如果需要从右往左迭代,用Array.prototype.reduceRight就好了

const arr = [[1, 2], [3, 4], [5, 6]];
// prevRes的初始值是传入的[],以后会是每次迭代计算后的值
const flatArr = arr.reduceRight((prevRes, item) => prevRes.concat(item), []);

console.log(flatArr); // [5, 6, 3, 4, 1, 2]

那这个compose方法要怎么实现呢,这里需要借助Array.prototype.reduceRight:

const compose = function(){
  // 将接收的参数存到一个数组, args == [multiply, add]
  const args = [].slice.apply(arguments);
  return function(x) {
    return args.reduceRight((res, cb) => cb(res), x);
  }
}

// 我们来验证下这个方法
let calculate = compose(multiply, add);
let res = calculate(10);
console.log(res);    // 结果还是200

上面的compose函数使用ES6的话会更加简洁:

const compose = (...args) => x => args.reduceRight((res, cb) => cb(res), x);

Redux 的中间件就是用compose实现的,webpack中loader 的加载顺序也是从右往左,这是因为他也是compose实现的。

  • pipe函数

pipe函数跟compose函数的作用是一样的,也是将参数平铺,只不过他的顺序是从左往右。只需要将reduceRight改成reduce就行了:

const pipe = function(){
  const args = [].slice.apply(arguments);
  return function(x) {
    return args.reduce((res, cb) => cb(res), x);
  }
}

// 参数顺序改为从左往右
let calculate = pipe(add, multiply);
let res = calculate(10);
console.log(res);    // 结果还是200
//ES6写法:
const pipe = (...args) => x => args.reduce((res, cb) => cb(res), x)

在ECMAScript 5(ES5)中,use strict 指令只能出现在脚本或函数的顶部。它不能出现在函数体内部,除非这个函数没有复杂的参数列表。所谓复杂的参数列表是指使用了默认参数、解构赋值或剩余参数(rest parameters)的函数。

function complexFunction(a = 1, ...b) {  
    'use strict'; // 这会抛出错误  
    // 函数体  
}  
// 或者  
function anotherComplexFunction({ x, y } = {}) {  
    'use strict'; // 这也会抛出错误  
    // 函数体  
}

# IIFE立即执行函数表达式

var a = 2;
(function IIFE( global ) {
  var a = 3;
  console.log( a ); // 3
  console.log( global.a ); // 2
})( window );
console.log( a ); // 2

IIFE 的一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去

参考 (opens new window)

最后更新: 11/7/2024, 8:26:17 AM