×

JavaScript 王国旅行的继续,连对象也没有怎么玩转“封装,继承,多态”?

96
BlindingDark
2017.07.25 12:03* 字数 2668

本文为 码农翻身 微信公众号投稿,未经码农翻身同意禁止转载


又见 JSON 酒馆

Java 小王子在 JavaScript 王国待了也有一段时间,这里虽然不像 Java 帝国那样规范严苛,但也因此千奇百怪,五光十色。

要说小王子最喜欢待的地方,那还是人来人往的 JSON 酒馆,不仅有上好的酒菜,还有机会认识到各式各样的人。这不,一来二去他已经和上回认识的眼镜官员成了朋友,甚至私底下还称兄道弟的。
(参见上回)

今天小王子又和眼镜大哥一起约来吃酒。才寒暄了几句,筷子还没动,酒馆门口就发生了一点骚动。只见来了一位看起来像是教书先生的精瘦男子,旁边还有些随从。
“...... 大哥,这人什么来头,气度不凡啊。”小王子悄声向旁边的眼镜官员问到。
“哈哈,他是函数式大主教,最近他们教派的信众激增啊,真是风水轮流转。”
“函数式?以前随父亲经商途中是听说过他们的事情,好像非常古老而且高深莫测啊,据说只有学者和虔诚的教徒才会加入他们,怎么最近也接收新人了?”
“小弟果然是见多识广,不错,在很久以前的确是这样,不过为何有大批新教徒这种事情也不在我的管辖范围内啊,所以也不太清楚。但是毕竟我是本国语言规范审查官,还是与他打过一些交道。不妨我们邀请他来一起喝酒,你亲自问他。”
小王子本来听到眼镜大哥也不了解情况正有些失望,突然得知可以直接面对面打探对方的底细,顿时兴奋了起来。
“那太好了!”

“这不是眼镜老弟么,别来无恙啊。”
“主教兄,甚好甚好,要不这顿我请?小二!”
饭菜上桌,互相客套了几句之后,话题就开始了。
“主教兄,这是我最近认识的朋友,年纪轻轻就周游四方,他有些事情要问你。”
“哦?”函数式主教把目光放了过来,“你有何打探?”
“久问贵教派向来神秘,为何最近有如此多的新教徒加入呢?”
“哈,这个嘛 ... 现在的年轻人都不喜欢条条框框,本教向来以简洁强大著称,自然就受欢迎了。”

小王子心想,这个主教倒也是有话直说自卖自夸,不过难道没了面向对象这种强大的武器,他还能变出怎么样的花儿来?
“那还敢请教大主教,依你看要怎么实现 Animal, Cat, Dog 这些对象呢?”想了一会儿,小王子认为不如直接发问。
“这个嘛,本教派并无对象这种说法,不过如果你愿意,也可以构造一个对象出来,只需要......”
“我们不用对象~”,还没等着主教说完,旁边的妹子突然发话了。
“哦,小莱,你来说吧。”这位被叫做小莱的少女像是主教的助手,看起来深得主教信任。
“嘻嘻,主教大人说的太复杂了,其实实现你说的那些根本不用什么对象。”
“哦?那该怎么做?”小王子顿时来了兴趣。


函数的翻身

“你想啊,搞出来这些猫啊狗啊的,不就是想让他们都可以吃东西么?干嘛要封装到一个对象中,太压抑了。”

let animal = { name: "animal" };

function eat(animal) {
    console.log(animal.name + " is eating");
}
eat(animal); // animal is eating

哦!Java 小王子恍然大悟,原来 JavaScript 里的方法根本不用强制放在对象中去声明,在这里函数已经翻身做主人,成为了一等公民,再也不用困在对象的牢笼中,声明后就可以直接使用。
“也就是说,猫狗都可以直接拿来调用咯?”

let dog = { name: "dog" };
let cat = { name: "cat" };

eat(dog); // dog is eating
eat(cat); // cat is eating

函数式“封装”

“是的呢~”小莱的脸上还是一样的笑容。
这样的话,继承和多态就变得毫无用武之地。函数直接操作的是数据,数据不同,结果自然就不同,好似“多态”的体现,而且由于函数成了通用的东西,无需继承大家都可以使用,如有需要“重写”,就干脆再写一个函数就行了。而且 JavaSript 是动态类型,根本不需要再抽象出来一个“接口”来统一描述某种抽象。
“嗯... ”小王子总觉得哪里不对,好像少了点什么,但又说不上来。“这是不是太自由了点... eat 本来只能是 animal 才能使用,现在任何东西都能用。这根本没有封装啊!”
“是的呢~ 只要有 name 都可以用哦~这样不是更好么?不过说到封装,我们的封装可更厉害哦~”

function createAnimal(name) {
    let animal = {};
    animal.name = name;
    return animal;
}
// 或者直接
function createAnimal(name) {
    return { name: name };
}
eat(createAnimal("human")); // human is eating

更奇怪的封装

没错,封装说到底是为了屏蔽细节实现,防止外界干扰。这么一来,外界就不需要了解 eat 和 creatAnimal 的实现原理。小王子很是吃惊,但不得不承认这是一种有效的办法。
“嗯~而且不仅可以封装一些数据,还可以对函数进行封装呢~”还没等小王子缓过神儿,轻快的声音又把他拉回现实。“我们函数式教派里面叫它闭包。”

function generateEatFunction(description) {
    function eat(animal) {
        console.log(animal.name + description);
    }
    return eat;
}

// 或者直接
function generateEatFunction(description) {
    return function(animal) {
        console.log(animal.name + description);
    }
}

let eatZH_CN = generateEatFunction(" 在吃东西~");
let xiaolai = createAnimal("小莱");

eatZH_CN(xiaolai); //小莱 在吃东西~

“啊?!”这次小王子彻底呆住了,信息量太大,他没有想到函数还可以这么使用,竟然直接作为返回值返回,还可以“封装”一些值进去。
看到呆住的小王子,小莱解释了起来,“嘿嘿,第一次见到的人都是这样子,没事,习惯了就好。在我们函数式里面呢,函数中可以嵌套函数,内层的函数经常需要引用外层函数中的一些值,而且可以把这个内层函数作为返回值呢~我肯定希望这个函数引用的外层值可以继续使用啊,这样多方便啊。所以 JavaScript 就对其进行了处理,如果返回的函数被引用,JavaScript 就会同时保留这个函数所引用的东西,并不会回收它们(比如这里的 description)。你们面向对象的人其实也经常在方法中声明局部变量是吧,但是你们并不会把一个函数返回出去,自然也就不会考虑方法执行完毕之后要保持哪些局部变量,直接回收掉就好了。”
“哦,这样啊...”小王子还是一脸似懂非懂的样子。

“来,我们做一下分解动作,首先 description 在 generateEatFunction 内部是一个局部量。”

function generateEatFunction(description) {
    // description == " 在吃东西~"
    ...
}

“然后内层的函数使用了这个局部量。此时一切都没有问题。”

function generateEatFunction(description) {
    function eat(animal) {
        // 这里引用了 description
        console.log(animal.name + description);
        // 相当于
        // console.log(animal.name + " 在吃东西~");
    }
    ...
}

“现在精彩的地方来了,我把这个函数返回了出去~当然我不希望这个局部变量被回收,我们依然希望它符合我的预期想法。”

function generateEatFunction(description) {
    function eat(animal) {
        // 这里引用了 description
        console.log(animal.name + description);
        // 相当于
        // console.log(animal.name + " 在吃东西~");
    }
    return eat;
    // 希望 eat 的行为可以像这样:
    // function eat(animal) {
    //     console.log(animal.name + " 在吃东西~");
    // }

}

let eatZH_CN = generateEatFunction(" 在吃东西~");

// 谢天谢地 JavaScrpit 的确没有回收掉那个局部量。
// 效果如同下面这样:
// eatZH_CN = function eat(animal) { console.log(animal.name + " 在吃东西~"); }
// eatZH_CN 成功指向了我们生成的函数。
// 接下来就是正常的调用啦~

eatZH_CN(xiaolai); //小莱 在吃东西~

迈向高阶

“嘿嘿,还不止这样呢~再给你看个东西~”小莱的话匣子一打开就停不下来,由不得小王子发呆,自顾自的继续介绍起来。“函数还可以作为参数传呢~ 这些都叫高阶函数~”

function each(list, fun) {
    for(let i = 0; i < list.length; i++){
        console.log(fun(list[i]));
    }
}

let animals = [dog, cat, xiaolai];

each(animals, eat);
// dog is eating
// cat is eating
// 小莱 is eating
// 

each(animals, eatZH_CN);
// dog 在吃东西~
// cat 在吃东西~
// 小莱 在吃东西~

“哇!传递进来的函数竟然真的就可以直接把它当作函数来使用!这太神奇了!”
“当然这里的 each 函数还不够函数式,其实可以用递归实现啦~毕竟函数式里面不提倡用数组下标来做遍历,但是到了 ES6 才有尾递归优化,所以... ”小莱还在说着......


未来

“咳咳,小莱你够了。客人都被你吓到了。”这时,在旁边默默看着的大主教说话了。
小王子还在震惊之中,不过毕竟是皇族,阅历丰富,还是悟出了一点门道,“我大概明白了一点,由于 JavaScript 是动态类型,其实无论是函数还是对象,在这里都可以做为一个值来传递。函数式里面偏向对值直接进行处理,通过对这些值的传递和组合,就可以组装实现更高级的功能。上面的那个 each 方法也展现出来另一种“多态”的体现。”
“不错不错,我看你这位朋友的来头可不一般啊,新加入的信徒大多都经过漫长的适应期才能理解,他却立即悟出这些道理来。”大主教对小王子赞赏有加,或许以他的智慧已经识破了小王子的身份。
“不不不,还是因为小莱妹妹讲的好啊。”小王子脸红的说道。
“这还不是多亏了我们的原型才能自由灵活的实现各种编程范式么。”眼镜大哥也参合了起来。
“哈哈哈,你又在自夸了。”
“来,说了这么多 eat,不说了,吃菜吃菜~”

看来编程世界上还有这么多种形态,小王子下次要去哪里,又会见识到怎样的东西呢?或许也能给他的 Java 帝国带回一些新鲜的血液?


登场人物名:
小莱 == lambda

码农翻身
Web note ad 1