函数对象的属性
我们知道JavaScript中函数也是一个对象,那么对象中就可以有属性和方法。
1 属性name:一个函数的名字我们可以通过name来访问;
function foo(params) {}
console.log(foo.name);//foo
2 属性length:属性length用于返回函数参数的个数;
function foo(n1, n2, n3, n4) {}
console.log(foo.length);//4
// 剩余参数不参与计数,给参数默认值也不算在里面
function foo(n1, n2, n3, ...args) {}
console.log(foo.length);//3
3 arguments转成数组
//方法一
var arr1 = Array.from(arguments);
var arr2 = [...arguments];
// 方法二
var arr3 = [];
for (var item of arguments) {
arr3.push(item);
}
// 方法三
var names:["acd","abc","cab","cba"];
names.slice();
//当我们这样去调用时候,slice函数内部this是names。而slice.call去调用时候就是为了改变this
var arr4 = Array.prototype.slice.call(arguments);//slice会返回一个新的数组。
理解JS纯函数
函数式编程:函数是第一公民,我们可以将函数作为返回值,在其它函数间互相传递,函数故此也是变得及其的重要,这种编程方式就是一种函数式的编程。
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
- 在react开发中纯函数是被多次提及的;
- 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
纯函数维基百科的定义:
在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
简单总结:确定的输入,一定会产生确定的输出;函数在执行过程中,不能产生副作用;
副作用的理解
副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一 些其他的副作用。
在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生 了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用,因为副作用往往是产生bug的 “温床”。
优点
- 可以安心的编写和安心的使用。
- 在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或 者依赖其他的外部变量是否已经发生了修改;
- 在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
纯函数案例
slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组。
const arr = [1,2,3,4,5,6];
const a = arr.slice(1,4);
console.log(a); // [2,3,4]
console.log(arr);//[1,2,3,4,5,6]
splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改。
const arr = [1,2,3,4,5,6];
const a = arr.splice(1,1);
console.log(a); // [2]
console.log(arr);//[1,3,4,5,6]
相比之下,slice就是一个纯函数,不会修改传入的参数;
纯函数练习
纯函数
// 相同的输入产生相同的输出,不会产生副作用。
function foo(s1,s2) {
return s1 + s2
}
非纯函数
// 例子一
var msg = '你好';
function bar() {
console.log('一些代码操作');
msg = '我好,大家好'; // 修改了外界的msg
}
bar()
// 例子二
function baz(info) {
info.name = 'cs2';
}
var obj = {
name:'cs',
age:18
}
baz(obj)
console.log(obj);//{ name: 'cs2', age: 18 }
JS柯里化
柯里化也是属于函数式编程里面一个非常重要的概念。
维基百科的解释:
- 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
- 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
- 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;
总结:
// 正常调用
function foo(x1,x2,x3,x4) {
return x1+x2+x3+x4
}
foo(1,2,3,4);
// 所谓的柯里化
function bar(x1) {
return function(x2) {
return function(x3) {
return function(x4) {
//进行操作
return x1+x2+x3+x4; // 10
}
}
}
}
bar(1)(2)(3)(4)
这里有一个函数,函数接收了多个参数,但是我们只处理一个(或前几个)参数(或者说:通过一部分的参数来调用它),然后返回一个函数,让返回的函数,去处理下一个参数,以此类推,直到参数被处理完成。
这样的一个过程就叫做函数的柯里化。
其实我们进行函数的柯里化之后,不论是运行还是说阅读性,都是变低了?那么为什么有这种写法?或者说它的优势体现在何处呢?
简化写法
var sum2 = x1 => x2 => x3 => x4 => x1+x2+x3+x4;
sum2(1)(2)(3)(4)
// 相当于是
var sum3 = x1 => {
return x2 => {
return x3 => {
return x4 => {
return x1+x2+x3+x4;
}
}
}
}
// 只是你的大括号和 return 可以省略。
柯里化的优势
让函数的职责单一
在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个 函数来处理;
那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;
在设计模式里面有一个原则:单一职责原则。
在面向对象的时候,如果我们去封装一个类,那么应该让这个类尽可能完成单一的一件事情。
举个例子
我们想函数里面传递三个参数,每个参数需要经过处理后再调用。
function foo(x) {
x = x +2;
return function (y) {
y = y * 2;
return function (z) {
z = z ** 2;
return x + y + z;
}
}
}
const res = foo(x)(y)(z);
console.log( res );
当然,这仅仅是一个简单的小例子,如果对于每一个参数的处理是很多行代码,那么所有的处理逻辑都放在一个函数里面,那么这个函数不仅变得非常的臃肿,而且函数具体那一步是干了什么事情,都比较难去发现的。
柯里化的复用
例子一
function sum(x,y) {
return x + y
}
// 每次调用都需要用到 10
console.log(sum(10,5));
console.log(sum(10,36));
console.log(sum(10,12));
console.log(sum(10,369));
我们能够发现,每次对于函数的调用来说,都需要添加上10这个参数,那么用柯里化怎样去表示呢?
/ 柯里化之后的函数
function makeSum(x) {
// 同时还可以对x进行逻辑处理
return function (y) {
return x + y
}
}
const make10 = makeSum(10);
const res = make10(5);
console.log(res);
柯里化之后多的函数,我们可以对逻辑进行一些复用。
例子二
function log(date,type,message) {
console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`);
}
log(new Date(),'debug','有问题');
log(new Date(),'info','警告了');
log(new Date(),'debug','有问题2');
我们能够发现,在每次进行传递参数的时候,net Date()都是必须要传递的,如果说type是一样的话,那么也可能造成重复传递的现象。
var log = date => type => message => {
console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`);
}
// 当前时间
const nowTime = log(new Date());
// err类型
const errType = nowTime('error');
errType('错误了1');
errType('错误了2');
// debug类型
const debugType = nowTime('debug');
debugType('debug1');
debugType('debug2');
// 当然也可以这样写
nowTime('debug')('info');
经过柯里化之后,看起来非常的简便,每一个模块的功能也能够一眼看穿,不论是对代码后期的修改,还是其它的操作,都是非常有益的。
自动柯里化函数
function foo(x1,x2,x3,x4) {
return x1 + x2 + x3 + x4;
}
// 封装一个函数,有人传入一个函数,返回一个柯里化后的函数。
function csChange(fn) {
// 必须要返回一个新的函数.curried柯里化
function curried(...args) {
// 剩余参数接收多个参数
// 判断接收到的参数个数,和原函数本身需要的参数个数
// 原函数的参数个数,可以通过 fn.length 拿到
if (args.length >= fn.length) {
// console.log(this);//window
return fn.apply(this,args);
}else{
// 参数没有达到个数之后,需要返回一个新的函数,来继续接收参数
function curried2(...args2) {
// 接收到参数后,需要递归去调用curried,来检查参数个数是否达到。
return curried.apply(this,[...args,...args2]);// 继续进行调用
}
return curried2
}
}
return curried
}
var newFoo = csChange(foo);
console.log(newFoo(10,20,30,40));
console.log(newFoo(10,20)(30)(40));
console.log(newFoo(10)(20)(30)(40));
小结:想要拿到某个函数的参数长度,那么就是函数名.length。
- 因为它返回的是一个函数,所以首先创建一个函数(curried),将这个函数给返回。
- 判断传入的参数长度和原来的参数长度是否相等,如果相等直接调用原来的函数,将接收到的参数放入原来的函数里,不相等进入下一步。
- 当参数的长度没有达到原来函数参数的长度后,还是需要返回一个函数(curried2),让我们能够继续的将参数传递进来。
- 将传入进来的参数和之前传递进来的参数进行合并传递给
curried,然后递归调用curried,当参数长度一致的时候,就会执行原来的函数。
组合函数
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:
- 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
- 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
- 那么是否可以将这两个函数组合起来,自动依次调用呢?
- 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);
例子
比如说,传入一个数字,先乘以2,然后再进行平方,返回最终的结果。
function double(n1) {
return n1 * 2
}
function square(n2) {
return n2 ** 2
}
var num = 10;
var res = square(double(10));
console.log(res);//400
使用组合函数,我们可以这样去完成。
function composeFn(...fns) {
// 边界判断,如果传入的不是一个函数
var length = fns.length;
for (var i = 0; i < length; i++) {
if (typeof fns[i] !== "function") {
throw Error(`index position ${i} not is a function`);
}
}
// 如果是函数
return function (...args) {
var res = fns[0].apply(this, args); //第一个函数
// 后面的函数
for (var i = 1; i < length; i++) {
var fn = fns[i];
fn.apply(this, [res]);
}
return res;
};
}
var newFn = composeFn(double, square);
var result = newFn(100);
with语句
所谓的with语句,就和我们平时的if语句一样,但是不一样的地方就是它是会形成自己的作用域的,而if则不是这样。
var message = '你好呀!';
var hot = '热';
var obj = {
name1:"cs",
message:'我是obj',
age:18
}
// ()写一个对象
with(obj){
// 先在with传入的 对象 里面去查找,没有找到去上一层,上上一层继续查找。
console.log(message);//我是obj
console.log(hot);//热
}
查找的顺序还是一样的,先从传入的对象里面查找,当找不到的时候,就会沿着作用域链一直向外查找。在严格模式下是不允许使用with的。
eval函数
eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行。
var mesaage = 'Hello Word';
console.log(mesaage);
// 使用eval
var JString = "var mesaage = 'Hello Word'; console.log(mesaage);";
eval(JString);// 全局函数
最后会在控制台进行输出。当然不建议在开发中使用eval,eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则),eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;eval的执行必须经过JS解释器,不能被JS引擎优化;
严格模式
在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):
- 是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
// 由于JS太过于灵活,所以这样写代码也是可以的。
msg = 'hello';
console.log(msg);//hello
严格模式对正常的JavaScript语义进行了一些限制:
- 严格模式通过 抛出错误 来消除一些原有的静默(silent,写的有问题,但不会造成恶劣影响)错误;
- 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
- 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;
开启严格模式
// 给某个js文件开启严格模式
"use strict";
// 给某个函数开启严格模式
function sayHello(){
"use strict";
}
当我们使用webpack等打包工具之后,会自动去开启严格模式的。
严格模式限制
- 无法意外的创建全局变量。
- 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常。
- 严格模式下试图删除不可删除的属性。
- 严格模式不允许函数参数有相同的名称。
- 不允许0的八进制语法。
- 在严格模式下,不允许使用with。
- 在严格模式下,eval不再为上层引用变量。
- 严格模式下,this绑定不会默认转成对象。
- 独立显示绑定this不传值的时候,这个时候
this不再是window,而是undefined。