文章目录
JavaScript柯里化
JavaScript中valueOf、toString的隐式调用
函数add可以实现连续的加法运算
函数add语法如下
add(num1)(num2)(num3)…;//注意这里是省略号哟,无限
使用举例如下:
add(10)(10)=20;
add(10)(20)(50)=80;
add(10)(20)(50)(100)=180;
请用js代码实现函数add。
function add(num){
var sum=num,
tmp=function(v){
sum+=v;
return tmp
};
tmp.toString=function(){
return sum
};
return tmp
}
alert( add(10)(20)(50) ) //f 80
起初没弄懂为什么会返回80,因为toString方法一直没被调用啊,怎么会返回sum呢;后来知道原来toString被隐式调用了,也就有了下文。
每个对象的toString和valueOf方法都可以被改写,每个对象执行完毕,如果被用以操作JavaScript解析器就会自动调用对象的toString或者valueOf方法,举栗子:
//我们先创建一个对象,并修改其toString和valueOf方法
var obj = {
i: 10,
valueOf: function() {
console.log('执行了valueOf()');
return this.i + 20
},
toString: function() {
console.log('执行了toString()');
return this.valueOf() + 20
}
}
//当我们调用的时候:
console.log(obj) //50 执行了toString() 执行了valueOf()
console.log(+obj) //30 执行了valueOf()
console.log(obj > 40) //false 执行了valueOf()
console.log(obj == 30) //true 执行了valueOf()
console.log(obj === 30) //false
//最后这个未输出任何字符串,个人猜想是这样的:全等比较时,js解析器直接先判断类型是否一样,明显一个是Object,一个是Number,所以直接不相等,根本不需要再去求值了。
由上可以看出,虽然我们没有调用obj的任何方法,但是要使用obj进行操作时,好像(其实就是,如果我们是创造js解析器的人这些就不是问题了,哎)js解析器自动帮我们调用了其toString或valueOf方法。
接下来就是探究什么时候执行的是toString方法,什么时候执行的是valueOf方法了。大致猜这样:如果做加减乘除、比较运算的时候执行的是valueOf方法,如果是需要具体值呢就执行的是toString方法。
JavaScript中valueOf函数与toString方法深入理解
基本上,所有JS数据类型都拥有这两个方法,null和undefined除外。它们俩解决javascript值运算与显示的问题。
var obj = {
i: 10,
valueOf: function() {
return this.i + 30;
},
toString: function() {
return this.valueOf() + 10;
}
};
console.log(obj > 20); // true
console.log(+obj); // 40
console.log(obj); // 50
之所以有这样的结果,因为它们偷偷地调用valueOf或toString方法。但如何区分什么情况下是调用了哪个方法呢,我们可以通过另一个方法测试一下。由于用到console.log,请在装有firebug的FF中实验!
var obj = {
i: 10,
valueOf: function() {
console.log('valueOf');
return this.i;
}
};
console.log(obj); // [object Object]
console.log(+obj); // 10 valueOf
console.log('' + obj); // 10 valueOf
console.log(String(obj)); // [object Object]
console.log(Number(obj)); // 10 valueOf
console.log(obj == '10'); // true valueOf
乍一看结果,大抵给人的感觉是,如果转换为字符串时调用toString方法,如果是转换为数值时则调用valueOf方法,但其中有两个很不和谐。一个是alert(’’+bbb),字符串合拼应该是调用toString方法,另一个我们暂时可以理解为===操作符不进行隐式转换,因此不调用它们。为了追究真相,我们需要更严谨的实验。
var obj1 = {
i: 10,
toString: function() {
console.log('toString');
return this.i;
}
};
console.log(obj1); // 10 toString
console.log(+obj1); // 10 toString
console.log('' + obj1); // 10 toString
console.log(String(obj1)); // 10 toString
console.log(Number(obj1)); // 10 toString
console.log(obj1 == '10'); // true toString
再看valueOf:
var obj2 = {
i: 10,
valueOf: function() {
console.log('valueOf');
return this.i;
}
};
console.log(obj2); // [object Object]
console.log(+obj2); // 10 valueOf
console.log('' + obj2); // 10 valueOf
console.log(String(obj2)); // [object Object]
console.log(Number(obj2)); // 10 valueOf
console.log(obj2 == '10'); // true valueOf
发现有点不同吧?!它没有像上面toString那样统一规整。对于那个[object Object],我估计是从Object那里继承过来的,我们再去掉它看看。
var obj3 = {
i: 10,
valueOf: function() {
console.log('valueOf');
return this.i;
}
};
console.log(obj3); // 10 valueOf
console.log(+obj3); // 10 valueOf
console.log('' + obj3); // 10 valueOf
console.log(String(obj3)); // 10 valueOf
console.log(Number(obj3)); // 10 valueOf
console.log(obj3 == '10'); // true valueOf
如果只重写了toString,对象转换时会无视valueOf的存在来进行转换。但是,如果只重写了valueOf方法,在要转换为字符串的时候会优先考虑valueOf方法。在不能调用toString的情况下,只能让valueOf上阵了。对于那个奇怪的字符串拼接问题,可能是出于操作符上,翻开ECMA262-5 发现都有一个getValue操作。嗯,那么谜底应该是揭开了。重写会加大它们调用的优化高,而在有操作符的情况 下,valueOf的优先级本来就比toString的高。
偏函数
探讨柯里化之前,我们先聊一聊很容易跟其混淆的另一个概念——偏函数(Partial Application)。在维基百科中,对 Partial Application 的定义是这样的:In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
其含义是:在计算机科学中,局部应用(或偏函数应用)是指将多个参数固定在一个函数中,从而产生另一个函数的过程。
举一个例子:假设一个工厂,用于生产梯形的零件,生产过程中我们要根据订单来源方给的一系列参数计算面积:
//声明一个计算梯形面积的函数:
function trapezoidArea(a,b,h){
return (a+b)*h/2;
}
//在订单中,大部分零件的高都是28的规格,此事经常调用面积函数就会变得冗余。
//我们便可以以第一个函数为模板,来创建存储了固定值的新的计算函数:
//声明一个固定高度的梯形面积计算函数
function trapezoidAreaByHeight28(a,b){
return trapezoidArea(a,b,28);
}
trapezoidAreaByHeight28(3,6); // (3+6)*28/2
当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:
//声明一个计算梯形面积的函数
function trapezoidArea(a,b,h){
return (a+b)*h/2
}
声明一个【可以生成固定高度的梯形面积计算】的工厂函数
function trapezoidFactory(h){
return function(a,b){
return trapezoidArea(a,b,h)
}
}
// 通过 trapezoidArea() 函数,生成绑定了固定参数的新的函数
const trapezoidAreaByHeight15 = trapezoidFactory(15);
const trapezoidAreaByHeight28 = trapezoidFactory(28);
trapezoidAreaByHeight15(6,13); //142.5
trapezoidAreaByHeight28(6,13); //266
也可以将其简化为:
const trapezoidAreaByHeight33 = (a, b) => trapezoidArea.call(null, a, b, 33);
trapezoidAreaByHeight33(6, 13); // 结果: 313.5
这里,我们就可以将 trapezoidAreaByHeight15() 、trapezoidAreaByHeight28() 和 trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。
偏函数的应用
偏函数往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数已被固定不会变更。偏函数的设计通常:
减少了参数相似性高的函数调用过程;
降低了函数的通用性,提高了函数的适用性,使其更专注于做某事;
减少了程序耦合度,提高了专有函数的可维护性。
柯里化(Currying)
柯里化(Currying)是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。而不同点在于,柯里化(Currying)通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1 的 n 个函数,并且支持连续调用。
例如:
//一个用于计算三个数字累加的函数
const addExample = function(a,b,c){
return a+b+c;
}
//调用
addExample(10,5,3); // 结果: 18
//通过柯里化,对上述函数进行演变
const addCurry = function(a){
return function(b){
return function(c){
return a+b+c;
}
}
}
//缔造新的 单一元 函数
const add10 = addCurry(10)
const add15 = add10 (5)
const add18 = add15 (3)
// 调用
add18(); // 结果: 18
可见,柯里化(Currying)用于将多元任务分解成单一任务,每一个独立的任务都缓存了上一次函数生成时传递的入参,并且让新生成的函数更简单、专注。上述演变也可以写作:
// 通过ES6箭头函数构造将更加简单
const addCurry = (a) => (b) => (c) => a + b + c;
// 调用也可以这样
addCurry(10)(5)(3); // 结果: 18
柯里化的应用
柯里化(Currying)分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:
// 创建一个用于检测数据类型的函数 checkType()
const checkType = (e, typeStr) => Object.prototype.toString.call(e) === '[object '+typeStr+']';
// 调用示范
checkType(12, 'Number'); // 结果:true
checkType(16.8, 'Number'); // 结果:true
checkType(NaN, 'Number'); // 结果:true
checkType(Infinity, 'Number'); // 结果:true
checkType('abc', 'String'); // 结果:true
checkType(true, 'Boolean'); // 结果:true
checkType({}, 'Object'); // 结果:true
checkType([], 'Array'); // 结果:true
checkType(null, 'Null'); // 结果:true
checkType(undefined, 'Undefined'); // 结果:true
checkType(checkType, 'Function'); // 结果:true
checkType(Symbol(), 'Symbol'); // 结果:true
使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:
// 检测值是否是 Number
const isNumber = (e) => checkType(e, 'Number');
// 检测值是否是 String
const isString = (e) => checkType(e, 'String');
// 检测值是否是 Boolean
const isBoolean = (e) => checkType(e, 'Boolean');
// 检测值是否是 Object
const isObject = (e) => checkType(e, 'Object');
// 检测值是否是 Array
const isArray = (e) => checkType(e, 'Array');
// 检测值是否是 Null
const isNull = (e) => checkType(e, 'Null');
// 检测值是否是 Undefined
const isUndefined = (e) => checkType(e, 'Undefined');
// 检测值是否是 Function
const isFunction = (e) => checkType(e, 'Function');
// 检测值是否是 Symbol
const isSymbol = (e) => checkType(e, 'Symbol');
柯里化无限调用
柯里化(Currying)分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数:
// 一个永远累加的函数,返回结果的新函数中缓存上一次调用,并进行数据累加
// 最终的数据依赖 success 回调函数获取
const alwaysAdd = function f1(nexter1){
const n1 = nexter1;
typeof n1.success == 'function' && n1.success(n1.value);
return function f2(nexter2){
const n2 = nexter2;
return f1( {value: n1.value+n2.value, success: n2.success} );
}
}
//调用方式:
const r1 = alwaysAdd({value: 2});
const r2 = r1({value: 5});
const r3 = r2({value:4, success: (result)=>console.log('结果: ', result)});
以这样的方式,我们构建的参数是一个简单对象 nexter,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。
一个简单的 Promise
Promise 对象无论是构造函数还是后续的链式调用中,都能看到柯里化设计的影子:接收单一参数,返回一个 Promise :
// 构建一个超级简单的 Promise 结构
class MyPromise{
constructor(executor) {
this.value = null;
typeof executor == 'function' && executor(this.resolve.bind(this), this.reject.bind(this));
}
then(success){
const result = success(this.value);
const mp = new MyPromise();
mp.value = result;
return mp;
}
resolve(value){
this.value = value;
}
reject(err){
this.err = err;
}
}
//调用方式为:
// 构建一个 MyPromise 对象
const mp1 = new MyPromise((resolve, reject) => {
resolve(10);
});
// 链式调用求值
mp1.then( r => {
console.log('mp1 r => ', r); // 结果: 10
return r + 3;
} ).then( r => {
console.log('mp2 r => ', r); // 结果: 13
return r + 5;
} ).then( r => {
console.log('mp3 r => ', r); // 结果: 18
} );
实现一个sum函数
实现一个sum函数,使其同时满足以下两个调用需求:
sum(2,3); //5 sum(2)(3); //5
function sum(...data) {
if (data.length > 1) {
return data.reduce((prev, next) => Number(prev) + Number(next))
} else {
return function(data1) {
if (data1.length > 1) {
sum(...data1)
}
return sum(data, data1)
}
}
}
console.log(sum(2, 3)) //5
console.log(sum(2)(3)) //5
实现一个twoSum函数
实现一个twoSum函数,传入源数组和目标数字,返回源数组中两个相加起来等于目标数字的索引:
twoSum([2,7,11,15],9); //[0,1],因为2+7=9
function twoSum(arr, count) {
const findIndex = arr.map((item, index) => {
const newArr = [...arr].slice(index + 1, arr.length)
const findNextIndex = newArr.findIndex(every => (count - item) === every)
return [index, findNextIndex === -1 ? findNextIndex : findNextIndex + index + 1]
})
return findIndex.find(item => !item.includes(-1))
}
console.log(twoSum([2, 7, 11, 15], 9))
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
在理论计算机科学中,柯里化提供了在简单的理论模型中比如只接受一个单一参数的lambda 演算中研究带有多个参数的函数的方式。