靠某省份的户籍很容易办到签证,好像是小程序里找的熊猫签证,买个套餐就行。如果条件不太好可以尝试办信用卡金卡,总之看签证套餐哪个容易办哪个。本来特定办了张招商金卡想加快办理流程,但是后来发现用自己户籍更快,就用要求户籍的套餐了。
当时还在开水团工作,同行的是开水团的同事朋友。有个相互照应也是挺好的。
我们用 Google Map 事先 mark 了感兴趣的景点和游览路线,见地图。
虽然有些景点最后并没有去成,但整个行程几乎都跟着计划来,所以我对旅游结果很满意。事先规划好非常有用。
从酒店来说,房间比较小,但什么都全,比较舒适。旅行只需要带衣服就行。刚到日本是住在浅草寺附近的一家酒店,后来是住新宿。
最好找对中国人友好的酒店,新宿那一家前台有个中年人会中文,沟通简单不少。
住的是大床房,但是同行的人不愿意一起睡,我也嫌床太软就睡硬一点的沙发了。
让我比较深印象的是酒店的厕所,不到两三平就整合了马桶浴缸洗手盆。马桶是比较电子化的,可以按钮用水冲菊花,不过我终究是没有尝试。
一般建筑很有特色。常见很窄但比较高的独栋。然后通常一层一个店,于是招牌也是很窄很高地贴在墙边。
看不到残破的建筑,听说是因为规定要定期交修缮费的缘故。
为省钱没怎么吃贵的东西。大部分时候是便利店买几个饭团,或者便当。
在歌舞伎町出来后,去了对面的一家拉面店,吃起来也就那样吧。对比中国的拉面,就是汤底味道好,面却是不及中国的。
吉野家好吃又便宜。便利店的便当便宜也不错。
按本人口味来看,吃的东西都比北京好,而且似乎对比本地的收入,便宜。
当然也不是都便宜。旅途后面有一天在台场附近的时候,我们想过去吃帝王蟹,终究还是觉得太贵而作罢。
全家/711/罗森周围都是,走十五分钟能遇到四五家…里面有常用药,喝的,吃的,日用品,漫画,八卦杂志,成人杂志…可以说靠便利店就能活下去了(感觉不少人的确是这样)。
店员服务态度非常非常好。第一次付费的时候,我用了特地申请的 visa,但怎么都刷不过,最后换了银联金卡才行。于是我就堵在了收银台前,但店员好像还很抱歉一样。
基本都是卖喝的,数量比便利店还多。一瓶喝的也就一百多日元吧。咖啡,汽水,有味道的饮料,都有。
日本的自来水是能直接喝(日语:水道水),这没什么奇怪的,中国的自来水在出厂的时候也似乎是可以喝的,只是输送的时候经过的管道不干净。所以日本这边是没有喝热水的习惯的,甚至店里提供的就是冰水。热水似乎就用来洗澡和泡面。
酒店里毕竟还是有煮水壶的,但我们也懒得用,在附近小超市买了不少打折的大瓶装モモ水——我俩叫它毛毛水——其实就是桃汁——直接当水喝了。
后来在一个公园里发现了一个饮水器,样子像一个洗手盘,立在远离树木的泥土地上,因此上方没有什么遮挡。出水的地方就是倒转的水龙头,水流向上喷,凑过嘴去喝即可。
年轻人基本不怎么会英文。老人反而会英文,比如秋叶原扭蛋会馆的老店长,新宿便利店的老店员。这就是所谓的昭和男儿,平成废物?
东京是个国际化城市,因此很多国外游客,尤其是欧美的,在街上一眼就能认出。
左行,但是习惯就好。但是上下楼梯有时右行有时左行,很奇怪。
一般很宽,不怎么堵车,车都很有礼貌会优先礼让行人,比广州的车还礼貌。什么你问北京?没被撞死就烧香吧。
路面很干净,是真的。至少在旅游去到的地方,无论早上晚上都基本看不到垃圾。特别厉害的是小街巷里也不太可能见到垃圾。但是街上垃圾桶很少,随身垃圾就只能要不到便利店外面扔要不拿回酒店房间扔。
好得离谱。首都最繁华的几个地方,银座新宿池袋涉谷尽管人多车多,但是空气质量好得根本无需检测,在北京经常上百的指数在这每天都是十。每天都是极其蓝色的天空和干净的白云。天空的颜色在照片里是不需要PS的。
虽然我没什么感觉,当时同行人某天说鼻子感觉很不舒服,估计可能是患了花粉症吧。
线路很多,去主要景点基本都有直达,不用担心交通问题(不像北京只有上下左右方向,又远又累)。但是搭乘比较复杂,同一个月台可以有三个方向的车,或者六种停靠方式的列车…最后一天我们坐错了三次车…
座椅全部是软座,真的非常舒服。
城市绿化很好。这次去了日比谷公园,新宿御苑公园,上野恩赐公园,代代木公园这四个。都是很大很漂亮的公园,很多古树,高树(两三层楼),没有任何垃圾,人不会特别多,吹着风舒服到感觉可以待一天。
旅游的时候是刚好遇到了綠之日(みどりの日,每年5.4),收费的公园免费了,挺爽。
普通人最好去银座新宿/池袋/涉谷/表参道,玩可以去迪士尼或者台场。从新宿出发,到这几个地方都只需半小时左右,不像北京那样去一个地方出行就占了一半时间般坑爹。优衣库总店在银座,有十层。
如果你喜欢ACG,在池袋和秋叶原,你想到没想到的都有,会迅速流失大量钱财,请注意。
药妆店很多地方都有,里面经常有中国人扫货代购。
西瓜卡很有用,可以对付地下铁(蓝色m字LOGO)/JR(绿色LOGO)/部分列车,看见IC卡就能刷,出门就靠它了。还能用在一些贩卖机上。
信用卡就不用说了,也请带多一点现金,至少一万日元。因为各类门票,非全球连锁的当地特色饭馆,西瓜卡充值,都需要现金。
银联用在免税店较多,实际通用程度不高。微信和支付宝也是差不多。
2023 更新,看现在日本的视频,支付宝微信好像发展得还可以,日本商家也期望中国游客爆买,估计现在这两种支付手段也可以轻松旅游吧。
也准备好会收到很多硬币,因为纸币最低面额是1000,500及以下都是硬币。
]]>三千年的存在时间和政权的多次更迭造成古埃及神明种类繁多,系统复杂,甚至可能互相矛盾,但总的来说有三个神话系统。
九柱神是古埃及宗教中一般都采用的神话系统。
太阳神,埃及神话中的最高神。生舒和泰芙努特。九柱神之一。
风神,拉的儿子,与泰芙努特生盖布和努特。九柱神之一。
雨神,拉的女儿,舒的妻子。九柱神之一。
大地之神,与努特生欧西里斯、赛特、艾西斯、奈芙蒂斯。九柱神之一。
天神,盖布的妻子。九柱神之一。
冥王,也是农业之神。九柱神之一。
死者的守护神,也是生育之神,奥西里斯的妻子。九柱神之一。
干旱之神,风暴之神。有时也被认为是战神。九柱神之一。
死者的守护神,赛特的妻子。九柱神之一。
赛特和奈芙蒂斯,负责制作木乃伊和评判死去的人该不该永生。
奥西里斯和伊西斯的儿子,一般是代表法老的权威。
爱神、美神,荷鲁斯妻子。
为木乃伊保存四个内脏。伊姆塞特保护肝脏,哈庇保护肺,杜阿木忒弗保护胃,克贝克塞努弗保护肠。
拉是众神之始,生出舒和泰芙努特。
舒与泰芙努特生盖布和努特。
盖布在下为地,努特在上为天,舒在中间撑着。
盖布与努特生奥西里斯、赛特、伊西斯、奈芙蒂斯。奥西里斯和伊西斯成夫妻,赛特和奈芙蒂斯成夫妻。
赛特嫉妒哥哥奥西里斯,举办了一个宴会,做一个刚好哥哥能躺进去的棺材,让别人躺进去试试。奥西里斯躺进去就封上扔进尼罗河溺死他。
伊西斯和奈芙蒂斯找回尸体复活,但赛特又一次杀死了奥西里斯并分尸十四块,藏在世界各地。伊西斯找了很久,除了下体凑足了尸体,负责做木乃伊的阿努比斯帮助她复活奥西里斯。伊西斯之后生下荷鲁斯,然后奥西里斯成了冥界之主。有些文献说是因为他的下体被鱼吃了,伊西斯使用木头代替导致复活不完全。
奥西里斯的经典造型是绿脸,双手握权杖交叉胸前,经常出现在法老为自己建造的陵墓门口。
荷鲁斯被秘密抚养长大之后,向赛特复仇并打败了他,过程中扯下了他的一颗睾丸,自己则失去一只眼睛。那只眼睛就被视为著名的「荷鲁斯之眼」。
八元神其实是四对分雌雄的神。关于八元神的故事并不多。
代表自然水,纳乌涅特(Naunet)和 努恩(Nu)、
代表空气或隐蔽或虚无,阿玛乌涅特(Amaunet)和 阿蒙(Amun)、
代表黑暗,卡乌凯特(Kauket)和 库克(Kuk)、
代表永恒或无限空间,哈乌赫特(Hauhet)和 胡(Huh)。
在这个体系中,拉是在四对神的相互作用下诞生的。
底比斯经常作为古埃及的首都,其当地的神话和神明在古埃及宗教上占有很重的地位,甚至排挤其他神明的地位。
本身只是地方神祇,但是十八王朝开始随着王室的崇拜和古埃及版图扩张而逐渐成为到了主神的地步。
有时又会和拉结合,成为众神之王。
阿蒙之妻,生下孔斯,一起成为底比斯三柱神。
阿蒙和姆特之子,一代月神。
不是太重要的神,负责守卫帝王谷。
智慧之神。二代月神同时也是数学、医药之神,负责守护文艺和书记的工作。相传他是古埃及文字的发明者。
Egyptian mythology
Ennead
Ogdoad_(Egyptian))
埃及神话
九柱神
八元神
埃及旅遊|一起認識複雜的埃及神祇吧!九柱神與重要神祇們
古埃及主要神話體系簡介
本文基本来源中英文维基,英文为主中文为辅,精简了大量不太重要的细节,比较适合快速了解和学习古埃及历史。如果对内容有什么疑问或者质疑,请务必留下评论或者联系我做讨论。
PDF 版见文章末尾。
时间上只会覆盖到中世纪之前,个人认为古埃及在阿拉伯人入侵之后就算是玩完了,所以之后的时期不做记录。
首先,整个古埃及基本就是沿着尼罗河建立的。尼罗河的走向是自南向北,古埃及也经常因为统治的原因,一段时期分成上埃及和下埃及,一段时期则是统一,正所谓分久必合,合久必分。上埃及是在南边即上游,而下埃及则是在北边即下游。
埃及学者一般根据托勒密王朝早期古埃及祭祀曼涅托的《埃及史》将古埃及历史分成八到九个时期,三十一个王朝(一个王朝不一定只有一位法老),而古埃及人则似乎是不划分历史时期的。另外考古出来的历史也各国不一,这里以维基为准。
这几个时期分别是前王朝时期、早王朝时期、古王国时期、第一中间期、中王国时期、第二中间期、新王国时期、第三中间期和古埃及后期。实际并不需要将所有的时期都记得一清二楚,只需要记得一些时期和王朝比较有名的事件和法老即可。
另外古埃及到了第三中间期之后的后期,已经无力回天,被外族来回入侵,再之后又来了马其顿和罗马,最后被阿拉伯完全控制后到现在基本就已经是一个穆斯林国家了。
古埃及年表在时间上的认定也不一,相差会有几十年,不过其开始一般都在 BC(before century,公元前)3100 左右。
点击图片查看清晰大图
经历了旧石器时代和新石器时代,大概 BC3600 年开始,尼罗河沿岸出现几十个「诺姆」,相互各自斗争。
诺姆:nome,源自希腊语 Νομός,意为 “行政区”;埃及语:Gau,中译「州」。
传说中美尼斯(Menes)统一了上下埃及开创古埃及王朝。但从考古证据上则很难支持有这么一个人,反而认为是纳尔迈(Narmer)才是统一埃及第一人,或者说两者是同一个。
包含一至二王朝,两个王朝,2 / 31。
现在看到的著名金字塔基本就在这个时期被建造。
此时期第一个法老(pharaoh)左塞王(King Djoser)开始建造阶梯金字塔(最常见最有名那种),位置在萨卡拉(开罗以南约 30 公里)。
阶梯金字塔据说是是伊姆霍特普(Imhotep)设计的。
伊姆霍特普,出身平民,但因智慧过人,学识渊博,受到法老的破格重用。他在整个法老时代受到崇拜,死后被尊为神,名号被刻在法老左塞雕像的基座上。…… 古埃及医学的奠基人…… 被誉为历史上第一位留下姓名的建筑师与医师,被奉为医学之神
维基百科
甚至有伊姆霍特普博物馆,见 维基,egyptsites 博客。
胡夫是第四王朝的第二位法老,是首位在吉萨建造金字塔的法老。今天去埃及旅游看的三座大金字塔就在吉萨,其中最大的就是胡夫下令修建。
胡夫金字塔塔高大概 146.5 米,现为大概 137 米,边长接近 230 米,由 230 万块巨石搭建而成,最重的可达 50 吨,最小的也有 1.5 吨。
胡夫金字塔是古代世界七大奇迹中最为古老和唯一尚存的建筑物。
有一个入口,但是现在被封禁,只使用某位哈里发在 CE820 开凿的盗墓通道作为入口。
尽管建造了最大的金字塔,但胡夫本人的雕像却是考古发掘中所有法老雕像中最小的。
卡夫拉是胡夫的孙子。尽管看起来卡夫拉金字塔比胡夫金字塔小一点,但是卡夫拉金字塔底座更高了 10 米,塔周边也更多附属设施。
狮身人面像斯芬克斯就属于附近的建筑,但并不是问路人问题杀人那只。
孟卡拉是第四王朝时期的第 16 位法老,孟卡拉金字塔远小于前两座金字塔,它的高度只有大约 65 米,总体积大约只有卡夫拉金字塔 1/10。
萨拉丁的儿子奥斯曼曾试图拆除孟卡拉金字塔,最后太过困难而作罢,给金字塔北面留下很大的垂直裂缝。
包含三至六王朝,四个王朝,6 / 31。
古王国时期后期出现严重干旱,国力下降,封建制度也削弱了中央权利,出现第一个黑暗时期,极其混乱的一个时期。这个时期法老权力被极度削弱,地方官员权利变大,在自己领地几乎就成了法老。
七八王朝极度混乱,史书记录不清。
古埃及也分成了上下埃及,下埃及经历九十两个王朝,上埃及则是十和十一王朝。最终上埃及由曼图霍特普二世(Mentuhotep II)向北进攻击败下埃及统治者再次统一古埃及,并继续主持十一王朝,进入中王国时期。
经历大概第七王朝到第十王朝,四个王朝,10 / 31。
十一王朝再次统一之后,开始重新收复失地,包括南边曾经在古王国和中间时期失落给努比亚的土地。
努比亚相当于今天埃及和苏丹交界位置。
十二王朝迁都底比斯(今卢克索)。
塞索斯特利斯三世(Sesostris III)是十二王朝法老,善战,向努比亚扩张,然后还建造了很多堡垒,被认为是这个王朝最强大的法老。
之后他的儿子阿蒙涅姆赫特三世(Amenemhat III)的统治时期被认为是中王国时期经济最好的时期。不过他从西亚邀请了希克索人(Hyksos)到尼罗河下游三角洲定居,也给后面十三十四王朝的结束埋下了隐患。其实十二王朝末期尼罗河洪水减少也为国家带来打击。
阿蒙涅姆赫特四世(Amenemhat IV)去世后,其子年幼,于是其姐妹塞贝克涅弗鲁(Sobekneferu)成为了历史考证上第一位女法老。她在位三年后去世,政权持续衰弱,是十二王朝最后的法老。
奥西里斯在这个时期成为了最重要的神。
经历十一王朝到十三王朝,三个王朝,13 / 31。
十三王朝的继续衰弱导致尼罗河三角洲(属于下埃及)的政权脱离并独立,是为十四王朝。统治者可能是迦南人(闪米特人)血统。
注意十三王朝和十四王朝几乎是共存的,直到 BC1650 希克索人全面控制下埃及,攻占了古首都孟菲斯。
希克索人对下埃及的统治被视为十五王朝,而南边的底比斯统治者也趁十三王朝的真空宣布独立并宣布十六王朝。
希克索人继续南下把十六王朝打败后,北退,上埃及建立十七王朝与希克索共存。
十七王朝学习希克索人的战术和武器,在十七王朝最后两个法老统治期间反攻北面希克索人。
阿赫摩斯一世(Ahmose I)是十七王朝最后一个法老的弟弟,继续父亲和兄长意志将希克索人赶出埃及,开创十八王朝,进入新王国时期。
包含十四王朝到十七王朝,共四个王朝,17 / 31。
这个时期有很多有名的法老。宗教上也有不少的变动。
在神明崇拜上,由于此时统一埃及十七王朝的统治者就是底比斯的家族,所以底比斯的地方神祇阿蒙(Amun)被推举到了主神的地位。由于古埃及一直以来一般都认为主神是太阳神拉(Ra),所以这段时期又经常将阿蒙和拉结合为同一个神,叫阿蒙-拉。
古埃及历史上最强盛的十八王朝就在这个时期内。
十八王朝第一任法老阿赫摩斯一世(Ahmose I),他登基的时候可能只有十岁,并可能在二十岁左右完成了「驱逐胡虏」,恢复了埃及对努比亚的统治。
之后是阿赫摩斯一世的儿子阿蒙霍特普一世(Amenhotep I)继位。
接下来的法老图特摩斯一世(Thutmose I)的身世则有点模糊,有可能是阿蒙霍特普一世的儿子,或者是他的军队指挥官。他意图扩大埃及版图,并第一个在帝王谷建造坟墓。
图特摩斯一世儿女中有一个儿子,图特摩斯二世(Thutmose II),是由妃子所生;其中有一个女儿,哈特谢普苏特(Hatshepsut),是由王后所生。
王后没有儿子,于是图特摩斯二世娶了他的姐姐哈特谢普苏特并登上王位,但很快就死了,而哈特谢普苏特只生了一个女儿,所以又从图特摩斯二世的妃子中找了个儿子当图特摩斯三世(Thutmose III)。
或许是因为图特摩斯三世太幼小,又或者是因为哈特谢普苏特觉得自己是正统王室之后,因此虽然作为摄政王,但是肯定是想自己当甚至极有可能当上了法老。而且考古学者从资料和建筑中考据,亦基本承认了她法老的地位(同时图特摩斯三世仍然在位)。
哈特谢普苏特在位期间的贡献主要为重新建立被希克索人入侵时破坏的贸易路线,以及大兴土木建造了很多建筑。她停止了土地扩张,使埃及在叙利亚及巴勒斯坦的统治权动摇(死后更丢失了统治权,但后来图特摩斯三世重新收复),但加强了和邻国的贸易,使埃及变得富庶。
她在卡纳克神庙建造了两个方尖碑,其中之一是埃及现存方尖碑中最高的,约 29 米高。
她在曼图霍特普二世神庙旁建造的哈特谢普苏特神庙,是古埃及建筑杰作以及热门景区。
经历王女、王后、摄政王和法老,哈特谢普苏特让人联想起中国的「武则天」。
图特摩斯三世在哈特谢普苏特统治二十一到二十二年后重新归来,并积极扩充军队和埃及版图,征服了地中海沿岸的以色列和叙利亚地区,甚至让邻国给其纳贡。图特摩斯三世也被后人称为「埃及的拿破仑」。
图特摩斯三世重获权力后,通过破坏纪念碑等方式极力抹除哈特谢普苏特的存在。后面的王朝在王表编纂上也似乎故意忽略了这个法老。
哈特谢普苏特和图特摩斯三世之间的关系,一般认为图特摩斯三世怨恨哈特谢普苏特。但最近一些研究进展称此说法不准确,并声称图特摩斯三世一直担当哈特谢普苏特的军事统治领袖,哈特谢普苏特也没有取其性命;而且损毁行动在图特摩斯三世统治晚期集中进行,哈特谢普苏特的资料也不是唯一被损毁的资料;从而推测图特摩斯三世是为了巩固继承人地位而作出如此的行为,他本人和他的继母并没有过节。这部分说法维基暂时没有找到证据支持。
埃及在图特摩斯三世以及几位法老的努力下,终于在阿蒙霍特普三世(Amenhotep III)治下达到艺术和国力顶峰。阿蒙霍特普三世一生建造了很多宏伟的建筑和雕像,其中有著名的门农巨像。门农巨像座落在是蒙霍特普三世神庙的门口守卫。而阿蒙霍特普三世神庙是埃及最大最华丽的建筑群,可是后来因为地震和被拆除的缘故,现在已经不存在了,剩下两尊残破的门农巨像。
帝国的强盛、对阿蒙神的崇拜和皇家经常修建神殿,令阿蒙神庙的祭司获得极大的财富和权力,祭司们不免出现腐败和干预朝政的行为。
于是阿蒙霍特普三世的儿子,阿蒙霍特普四世,相信是为了打击祭司和权贵,进行了宗教改革。他简化多神系统,要求全国改为崇拜唯一的太阳神阿顿(Aten),甚至把自己的名字改为阿肯那顿(Akhenaten),迁都埃赫塔顿(Akhetaten),摧毁阿蒙神庙。有学者认为这表明了他创造了世界上最早的一神教。但他沉迷新兴宗教事务,以至于对边疆情况置若罔闻,而此时地中海北面的赫梯人正值盛时。于是古埃及逐渐失去了西亚地区的影响力。
他的大皇后,娜芙蒂蒂(Nefertiti),很有可能和他一同统治埃及。
阿肯那顿的后继者在他死后又把旧宗教恢复回来,并大力抹消新兴宗教的记录和影响。
阿肯那顿之后经过了一个或者两个短暂统治的法老(可能是他儿子或者王后摄政),王位传到了最著名的法老——图坦卡蒙(Tutankhamun)。
图坦卡蒙曾用名图坦卡顿(Tutankhaten),从名字的更改可以看出信仰的更改。他将首都迁回底比斯,重开神庙,重新恢复阿蒙神的崇拜。
图坦卡蒙的出名是因为所有法老的坟墓都几乎被盗空了,而唯有他在帝王谷的坟墓三千多年来从没被盗,以至于发掘出近五千件文物,令法老以及图坦卡蒙成为流行文化。
关于有名的「法老的诅咒」,可以说没有任何证据表明诅咒存在,坟墓内也没有发现任何诅咒的存在。打开坟墓和棺材的在场 58 人,据研究只有 8 - 10 人在十几年内死亡,根本不足为诅咒的依据。
2019 年(本年) 1 月,图坦卡蒙坟墓向游客开放。
然而图坦卡蒙似乎一直疾病缠身,18 岁就死了,死因有各种猜测,包括疟疾、腿疾或者被谋杀。他与其异母姐姐安克姗海娜曼的两个女儿也早年夭折,因此图特摩斯家族就绝后了。安克姗海娜曼似乎曾经写信给赫梯国王苏庇路里乌玛一世要求他要一个王子嫁给她,但是王子还没到就被杀了。
之后是阿肯那顿和图坦卡蒙大臣,甚至可能是其背后的操纵者,阿伊(Ay),极有可能娶了安克姗海娜曼,当上了法老,统治了一个很短暂的时期。之后就被图坦卡蒙的将军,霍朗赫布(又或者哈伦海布,Horemheb)夺取了法老位置。
霍朗赫布以孟菲斯为首都,继续抹除阿顿以及其前任等人的痕迹,算是个解决宗教动荡和国家分裂等麻烦的人。
霍朗赫布跟王室没有关系,也没有儿子,法老位置传给了当时的大臣门帕提拉(原名普拉美斯,Paramesse),即十九王朝的第一任法老拉美西斯一世(Ramesses I)。
拉美西斯一世的儿子塞提一世(Seti I)和她父亲重建了王国的秩序,并在叙利亚和迦南地区打击赫梯人的势力。从纪念碑上可以找到他的伟大功绩,虽然一般来说都是倾向于夸大。总的来说,塞提一世可以说是恢复新王国时期的荣光。
塞提一世也兴建了很多建筑,包括位于底比斯的塞提一世祭庙、位于阿拜多斯的塞提一世纪念庙以及大柱式大厅,虽然大部分应该是在拉美西斯二世时期完成的。
阿拜多斯的塞提一世纪念庙虽然外表其貌不扬,但是内部壁画精美,而且有不少壁画仍然有颜色。其中一面墙上按时间顺序记录了大多数王朝的法老的名字,从美尼斯到塞提一世共 76 个,被称为阿拜多斯王表,是后世研究历史的主要来源之一。
塞提一世的儿子就是赫赫有名的拉美西斯二世(Ramesses II)了。他如此的有名以至于后来有九位法老使用了拉美西斯这名字。他在位长达 66 到 67 年,执政的时期是新王国最后的强盛年代。
但凡古埃及强盛的时期,统治的法老无不是领土争端和宏伟建筑这两方面有所建树,拉美西斯二世也不例外。他打败从地中海入侵的海盗,向东北占领迦南地区和南叙利亚地区,和北面的强敌赫梯国王穆瓦塔利二世(Muwatalli II)来回地打拉锯战,最后分庭抗礼;稳定南边的努比亚。他在位时间比较长,也在埃及各地建造非常多的建筑,甚至在不是他建造的建筑上也留下标志。
卡迭石战役是埃及和赫梯之间比较著名的战役。战役的情况很可能是这样:埃及想进军占领卡迭石,但被赫梯战车袭击并击溃;法老在营地受困的时候雇佣兵到了,反击了赫梯,导致赫梯军败退;埃及虽然赢了这场战斗但是也无法攻克卡迭石,战略上算是输了。之后双方一直僵持不下。卡迭石战役有非常详细的记录,但基本都是埃及方面一面之词,因此也存在夸大拉美西斯二世的可能性。
之后穆瓦塔利二世病逝,拉美西斯二世和继位的哈图西里三世(Hattusili III)缔结埃及赫梯和约,此时距离卡迭石战役已经十五六年了。
埃及赫梯和平条约,或称卡迭石条约、永恒条约、银条约,是古代近东(即今西亚、中东地区)地区协议双方都有保存下来的最古老的条约。缔结和约结束了长期的冷战实际上对签署双方都有利益。埃及的目的,很可能是为了吹嘘法老功绩,并成立军事同盟以共同对抗西亚更东边的新贵力量亚述;而赫梯的目的,则可能是新王为了巩固地位以及利用法老的国际影响力提升自己的国际地位。
拉美西斯二世的闻名也许也来自于他遍布埃及的大型建筑和到处刻画自己的荣光事迹。
比较有名的建筑是拉美西姆神庙、哈布城神庙和阿布辛贝勒神庙。
拉美西姆神庙现今已经是废墟,往日的样子只能靠门口和后面的建筑来想像了。
比较值得注意的是庙内有一副拉美西姆国王名单(Ramesseum king list),列出了新王朝的大部分法老。
哈布城神庙(Medinet Habu)离哈特谢普苏特神庙不远,很多法老都在这修建过建筑,刻过壁画。然而拉美西斯二世应该是其中最张扬的了,似乎很害怕被人遗忘。
拉美西斯二世本身就喜欢乱改别人的壁画,因此他也害怕别人抹掉他的壁画,于是他的壁画都刻得非常深。
阿布辛贝勒神庙(Abu Simbel temples)应该是很多人从电视媒体等看到过的神庙之一,整个神庙在岩石上开凿而成,门口四个雕像也是其标志之一。
神庙东北面是法老为哈索尔(Hathor,古埃及女神)和妮菲塔莉(Nefertari,拉美西斯二世的大王后)所建的小庙(the Small Temple)。妮菲塔莉的雕像跟拉美西斯二世一样高,表明地位跟他几乎平起平坐,也是几乎唯一一位在世就被神格化的埃及王后。其他同样有名的王后是克利奥帕特拉七世(埃及艳后)、娜芙蒂蒂(阿肯那顿的大王后)和哈特谢普苏特(埃及「武则天」)。
近代由于在阿斯旺兴建水坝,联合国筹钱将阿布辛贝勒神庙和小庙搬离到比原地高 200 米处。
频繁的战争、大兴土木,造就了巨大的国库开销,加大了国力下降。在拉美西斯二世死后,埃及就立刻开始走下坡路。
过了几任法老后,到了第二十王朝第二任法老拉美西斯三世的统治。他抵抗了几次地中海和利比亚的入侵,治下还出现了人类历史上第一次有记录的劳工罢工。他的一个妃子还曾试图毒杀他,最后是失败了。
拉美西斯五世,土地和财政基本就已经被阿蒙神庙的祭司所控制了。
拉美西斯六世在建造坟墓时无意间将图坦卡蒙坟墓埋在了地下,避免了其日后被盗掘。
第二十王朝,古埃及陷入内忧外患,尼罗河水位下降、内政动乱、法老坟墓被盗、官员腐败,又丢了叙利亚和巴勒斯坦,最后法老基本就没有统一埃及的权力了。
包含十八王朝到二十王朝,共三个王朝,20 / 31。
拉美西斯十一世死后,结束二十王朝,斯门代斯一世(Smendes I)开创二十一王朝,然而此时法老权力已经极弱,基本被赶到下埃及去了,上埃及和中埃及地区则是由底比斯阿蒙神庙的大祭司们所控制。
之前二十王朝已经在尼罗河三角洲定居的利比亚人,在舍顺克一世(Shoshenq I)统领下统一埃及,创立二十二王朝,这里的法老已经不是本土埃及人了。然后二十三王朝二十四王朝,王室和内政仍然比较动荡,南方起源于库施(kush)的努比亚王国趁乱北上把整个埃及打了下来,赶走了利比亚人,建立二十五王朝。
努比亚人重新恢复了古埃及的的宗教传统,修复和建造了不少寺庙和纪念碑,还在家乡(位于现在的苏丹)重新建造起金字塔来。
包含二十一王朝到二十五王朝,共五个王朝,25 / 31。
之后西亚的亚述人开始入侵古埃及,努比亚人不敌,向南退回努比亚。亚述人可能对占着古埃及没有兴趣,在洗劫一番后退了回去,扶植了普萨美提克一世(Psammetichus I)作为法老。
之后,普萨美提克一世趁亚述帝国忙于战乱,联合古希腊的雇佣军,又重新统一了古埃及,建立二十六王朝,恢复了繁荣稳定。之后亚述被内部独立的新巴比伦帝国推翻,普萨美提克一世也曾想恢复西亚的霸权,但被尼布甲尼撒二世(Nebuchadnezzar II)统治的新巴比伦帝国打了回来。
那个古埃及法老用婴儿做实验,实验听不到别人说话怎么学语言的故事,就是普萨美提克一世的故事,记录在希罗多德写的书「历史」第二卷中。
然而二十六王朝已经是最后一个埃及本土王朝了。更东边的波斯阿契美尼德帝国(波斯第一帝国)灭了新巴比伦帝国,接着吞并了古埃及,波斯国王冈比西斯二世(Cambyses II)成了法老,称为二十七王朝。
之后二十六王朝后裔推翻波斯的统治,先后建立短暂的二十八、二十九、三十王朝。
然后波斯人再次征服古埃及,是为三十一王朝。
包含二十六王朝到三十一王朝,共六个王朝,31 / 31。
当无人不知的亚历山大击溃波斯大军并来到埃及的时候,根本没有遇到什么抵抗,埃及当地的波斯管理者就直接将埃及献给他了,埃及人民还视其为埃及的解放者。
亚历山大尊重当地信仰,去绿洲朝圣得到神谕。神谕宣称他是阿蒙的儿子。实际上就是得到了祭司等有权势的人的承认。他成立了一个新的希腊城市名为亚历山大(他到哪就在哪建亚历山大城),并任命希腊人——而不是埃及人——作为高官。
亚历山大没有在埃及待很久,就去征服其他地方了,从此再没有回来过。他的部下托勒密留在埃及统治,并从亚历山大死后分崩离析的亚历山大帝国中独立出来,创立托勒密王朝统治了近三百年。
托勒密王朝中,男性都叫托勒密(Ptolemy),女性一般叫克利奥帕特拉(Cleopatra)、贝勒尼基(Berenice)和阿尔西诺伊(Arsinoe),因此所有统治者都是这四个名字的加几世这样的称呼。最为人知的就是末代法老,埃及艳后,克利奥帕特拉七世。
托勒密王朝没有使用希腊文明取代埃及文明,反而扶持其延续,修建埃及风格的神庙,维护传统宗教仪式。当然也带来了希腊文明的影响,这个时期很多文艺作品都有两个文明融合的风格。
罗塞塔石碑本来并不是什么比较特别的石碑,只是托勒密五世加冕一周内纪念所制,但碑上同时使用了三种语言,分别是圣书体(埃及象形文字)、埃及草书(世俗体)和古希腊文,令后世学者可以做参考比对,通过古希腊文来解读圣书体,从而使这块石碑无比珍贵,也因此罗塞塔石碑在现代也被引申为「暗喻」、「翻译」和「关键线索」等的含义。
克利奥帕特拉七世是托勒密王朝中第一个学会埃及语并接受埃及信仰和埃及神明的人,这是其他王室成员所拒绝的事(想想罗塞塔石碑还需要古希腊文书写)。她早年在就政治上活跃,应该算是被迫和托勒密十三世(同父异母弟弟)结婚以一同统治埃及。但之后两人发生冲突,艳后被迫流亡叙利亚,招兵买马打算反攻托勒密十三世。
此时罗马内部也正在争斗,凯撒打败了庞培,后者被迫跑到埃及。而托勒密十三世可能希望寻求凯撒支持而擅自将凯撒老对手庞培暗杀了,在凯撒追到埃及时献上庞培的头颅。这可能令凯撒十分不满,因为庞培是他的劲敌、女婿,也是罗马执政官,不应该被异国人杀死。
克利奥帕特拉七世可能看准了这个机会潜回埃及,并将自己献给凯撒,做他的情人,取得凯撒的支持。之后凯撒对埃及统治者的仲裁,是克利奥帕特拉七世上位。
传说当时克利奥帕特拉把自己伸直,用毯子卷起来包覆其中,命人抬着进入王宫,这时克利奥帕特拉年仅21岁,凯撒52岁。
维基百科
托勒密十三世当然十分不满,率军围攻亚历山大城内罗马军。凯撒等到增援到来,脱围之后在尼罗河战役打败托勒密十三世,随后另立了托勒密十四世(仍旧是艳后的弟弟)和艳后结婚统治埃及。艳后虽然名义上嫁给托勒密十四世,但是实际上却和凯撒相好,还生了一个孩子托勒密·凯撒。
BC46,艳后来到罗马,遭到罗马人民的厌恶,因为凯撒已婚,而她和凯撒关系暧昧。但凯撒不愧为独裁者,完全罔顾旁人,甚至为艳后制作黄金雕像,和罗马人先祖维纳斯神像一起摆放。在凯撒被刺后,艳后还留在罗马,可能是希望自己儿子继承凯撒。但是凯撒大概没有承认这个儿子,而是在遗嘱另立他的养子屋大维作为继承人。于是她回到埃及,毒死了托勒密十四世,与其儿子共同统治埃及。
安东尼是凯撒生前最重要的军队指挥官。凯撒被刺后,他和屋大维解决了反凯撒势力。之后在埃及传唤艳后的时候也应该是被迷住了,和她在 BC41 到 BC40 年度过了一段时光。接着安东尼离开了埃及,艳后不久后生下一男一女双胞胎,相信就是安东尼的儿女。
安东尼和屋大维从 BC41 年开始就不和,因为安东尼妻子发起了和屋大维对抗的战争。虽然后来妻子突然身亡,安东尼也娶了屋大维的姐姐小屋大薇以维持稳定,但是两人仍然是竞争关系。安东尼把大量土地给予了艳后,屋大维利用这一点宣称其为外国女王而牺牲共和国权利。两人关系持续恶化,安东尼也冷落小屋大薇。之后对安息帝国的战争失利,安东尼回到埃及。BC36 年,艳后又为安东尼生下第二个儿子。
BC34 年赢得对亚美尼亚的战争后,安东尼和艳后在大胜后的举动被屋大维大肆利用,煽动罗马对安东尼的不满,包括艳后以神的名义宣称自己和凯撒的儿子是万王之王,自己则是万王之女王,和安东尼的儿女则分别册封国王等。安东尼更公然向罗马宣称将包括打下亚美尼亚等一部分罗马行省赠予克利奥帕特拉七世。最冒犯屋大维的,是安东尼宣称艳后和凯撒的儿子托勒密·凯撒才应该是凯撒的继承人。
屋大维借机行事向埃及女王宣战,在亚克兴角海战中打败安东尼。安东尼输掉海战的原因可能是因为海军在战事胶着时出现叛逃,安东尼和艳后逃回了埃及,陆军见势也投降屋大维了。
屋大维乘胜追击到埃及,退无可退的安东尼误认为艳后已自杀,用剑刺自己,可能是被带到艳后藏身的坟墓才真正死去。艳后随后也被捕了。
关于克利奥帕特拉七世的死法,无人知道。流传最广的版本是她让毒蛇咬自己而毒发身亡,也有观点认为他是被屋大维下令处死。
她和凯撒的儿子托勒密·凯撒被屋大维处死,从此埃及成为了罗马帝国的行省,托勒密王朝终结,埃及的法老时代也终结了。
作为罗马帝国行省的埃及,发生的事情属于罗马的历史了,这里不详述。只是 CE264 年的时候,反抗罗马的帕尔米拉女王季诺碧亚征服了埃及使其脱离罗马,并自称埃及女王,说她的家族可追溯到克利奥帕特拉七世。后来罗马也没有战胜她,只能是围城断粮逼其投降。
埃及有一段时间曾被萨珊王朝所占领(CE621 - CE629),拜占庭(东罗马帝国)虽然重新夺回,但十年后阿拉伯帝国入侵的时候,已经无力抵抗,自此埃及伊斯兰化,埃及文明也不复存在。
古埃及是一个宗教特色很浓的国家,了解其神明系统有助于深入理解古埃及,不过没有兴趣也可不看。
见 古埃及神明 一文。
法老的头冠分两部分,代表上埃及的白色王冠,代表下埃及的红色王冠,一般法老会戴着两个,诏示其为整个埃及的统治者。
古埃及神话中的神明都是兄妹 / 姐弟结合,这可能造成了古埃及王室基本都是近亲结合。如果侧室的儿子登基法老,一般也要娶正室的女儿。统治者可能借此保持所谓的血统纯正。
建造金字塔和神庙的大石头、花岗岩等,很多是从阿斯旺开凿的。
开放世界埃及篇
古埃及 - 维基百科,自由的百科全书
古埃及历史 - 维基百科,自由的百科全书
阿姨学、古埃及 | 阿姨学词典 Wikia | FANDOM powered by Wikia
History of ancient Egypt - Wikipedia
Prehistoric Egypt - Wikipedia
Early Dynastic Period (Egypt) - Wikipedia
Old Kingdom of Egypt - Wikipedia
First Intermediate Period of Egypt - Wikipedia
Middle Kingdom of Egypt - Wikipedia
Second Intermediate Period of Egypt - Wikipedia
New Kingdom of Egypt - Wikipedia
Third Intermediate Period of Egypt - Wikipedia
Late Period of ancient Egypt - Wikipedia
Ptolemaic Kingdom - Wikipedia
Sasanian Egypt - Wikipedia
First Dynasty of Egypt - Wikipedia
Second Dynasty of Egypt - Wikipedia
Third Dynasty of Egypt - Wikipedia
Fourth Dynasty of Egypt - Wikipedia
Fifth Dynasty of Egypt - Wikipedia
Sixth Dynasty of Egypt - Wikipedia
Seventh Dynasty of Egypt - Wikipedia
Eighth Dynasty of Egypt - Wikipedia
Ninth Dynasty of Egypt - Wikipedia
Tenth Dynasty of Egypt - Wikipedia
Eleventh Dynasty of Egypt - Wikipedia
Eleventh Dynasty of Egypt - Wikipedia
Twelfth Dynasty of Egypt - Wikipedia
Thirteenth Dynasty of Egypt - Wikipedia
Fourteenth Dynasty of Egypt - Wikipedia
Fifteenth Dynasty of Egypt - Wikipedia
Sixteenth Dynasty of Egypt - Wikipedia
Abydos Dynasty - Wikipedia
Seventeenth Dynasty of Egypt - Wikipedia
Eighteenth Dynasty of Egypt - Wikipedia
Nineteenth Dynasty of Egypt - Wikipedia
Twentieth Dynasty of Egypt - Wikipedia
Twenty-first Dynasty of Egypt - Wikipedia
Twenty-second Dynasty of Egypt - Wikipedia
Twenty-third Dynasty of Egypt - Wikipedia
Twenty-fourth Dynasty of Egypt - Wikipedia
Twenty-fifth Dynasty of Egypt - Wikipedia
Twenty-sixth Dynasty of Egypt - Wikipedia
Twenty-eighth Dynasty of Egypt - Wikipedia
Twenty-ninth Dynasty of Egypt - Wikipedia
Thirtieth Dynasty of Egypt - Wikipedia
Argead dynasty - Wikipedia
Ptolemaic Kingdom - Wikipedia
Valley of the Kings - Wikipedia
Giza pyramid complex - Wikipedia
Histories (Herodotus) - Wikipedia
Assyria - Wikipedia
Kingdom of Kush - Wikipedia
Hyksos - Wikipedia
Ramesseum - Wikipedia
Ancient Near East - Wikipedia
Abu Simbel temples - Wikipedia
Egyptian–Hittite peace treaty - Wikipedia
Battle of Kadesh - Wikipedia
Abydos King List - Wikipedia
Abydos, Egypt - Wikipedia
Canaan - Wikipedia
Karnak - Wikipedia
早王朝时期 - 维基百科,自由的百科全书
古王国时期 - 维基百科,自由的百科全书
第一中间时期 - 维基百科,自由的百科全书
中王国时期 - 维基百科,自由的百科全书
第二中间时期 - 维基百科,自由的百科全书
新王国时期 - 维基百科,自由的百科全书
第三中间时期 - 维基百科,自由的百科全书
古埃及晚期 - 维基百科,自由的百科全书
阿契美尼德王朝 - 维基百科,自由的百科全书
托勒密时期 - 维基百科,自由的百科全书
罗马及拜占庭时期 - 维基百科,自由的百科全书
萨珊时期 - 维基百科,自由的百科全书
埃及第一王朝 - 维基百科,自由的百科全书
埃及第二王朝 - 维基百科,自由的百科全书
埃及第三王朝 - 维基百科,自由的百科全书
埃及第四王朝 - 维基百科,自由的百科全书
埃及第五王朝 - 维基百科,自由的百科全书
埃及第六王朝 - 维基百科,自由的百科全书
埃及第七王朝 - 维基百科,自由的百科全书
埃及第十一王朝 - 维基百科,自由的百科全书
埃及第十二王朝 - 维基百科,自由的百科全书
埃及第十三王朝 - 维基百科,自由的百科全书
埃及第十四王朝 - 维基百科,自由的百科全书
埃及第十五王朝 - 维基百科,自由的百科全书
埃及第十六王朝 - 维基百科,自由的百科全书
埃及第十七王朝 - 维基百科,自由的百科全书
埃及第十八王朝 - 维基百科,自由的百科全书
埃及第十九王朝 - 维基百科,自由的百科全书
埃及第二十王朝 - 维基百科,自由的百科全书
埃及第二十一王朝 - 维基百科,自由的百科全书
埃及第二十二王朝 - 维基百科,自由的百科全书
埃及第二十三王朝 - 维基百科,自由的百科全书
埃及第二十四王朝 - 维基百科,自由的百科全书
埃及第二十五王朝 - 维基百科,自由的百科全书
埃及第二十六王朝 - 维基百科,自由的百科全书
埃及第二十七王朝 - 维基百科,自由的百科全书
埃及第二十八王朝 - 维基百科,自由的百科全书
埃及第二十九王朝 - 维基百科,自由的百科全书
埃及第三十王朝 - 维基百科,自由的百科全书
埃及第三十一王朝 - 维基百科,自由的百科全书
古希腊 - 维基百科,自由的百科全书
馬其頓王國 - 维基百科,自由的百科全书
托勒密王朝 - 维基百科,自由的百科全书
波斯 - 维基百科,自由的百科全书
帝王谷 - 维基百科,自由的百科全书
吉萨金字塔群 - 维基百科,自由的百科全书
历史 (希罗多德) - 维基百科,自由的百科全书
亚述 - 维基百科,自由的百科全书
库施 - 维基百科,自由的百科全书
喜克索斯人 - 维基百科,自由的百科全书
拉美西姆 - 维基百科,自由的百科全书
古代近东 - 维基百科,自由的百科全书
阿布辛贝勒神庙 - 维基百科,自由的百科全书
埃及赫梯和约 - 维基百科,自由的百科全书
卡迭石战役 - 维基百科,自由的百科全书
阿拜多斯王表 - 维基百科,自由的百科全书
阿拜多斯 - 维基百科,自由的百科全书
迦南 - 维基百科,自由的百科全书
卡纳克神庙 - 维基百科,自由的百科全书
No Silver Bullet
几乎每一个编程语言都有 for
,JavaScript 也不例外。
在 JavsScript 中,for
广泛用于遍历数组中,也能用于遍历对象的属性。
语法:
1 | for ([initialization]; [condition]; [final-expression]) |
initialization
是初始化语句,通常用于初始化计数变量(比如,你们最爱的 i
);condition
是判断本次是否执行 statement
;final-expression
在 statement
执行完后执行,通常用于对计数变量进行变换。
例如,要遍历一个数组 arr
,那么可以这样写:
1 | for (var i=0; i<arr.length; i++) { |
或者经常见到所谓的性能优化:
1 | for (var i=0, len=arr.length; i<len; i++) { |
强烈建议使用
const
或let
声明变量而不是使用var
,因为var
会在for
语句外声明变量,结果就是变量可能会在意外的地方被读取到。如果你不能使用 ES2015 或更新的版本,下文同样有解决方法(同样是本文的主要内容)。
如果你写过 C 系列,那么你有可能忍不住自己的麒麟臂,写出「炫技」的代码来。比如 MDN 上的这个例子。
1 | function showOffsetPos(sId) { |
这么写看着很酷,但是实际上不要这样写,尤其是在团队合作中。这样的代码一是混杂难懂,二是难以维护。代码首先是写给人看的,接着才是给机器运行的。
或许你已经非常习惯写 for
了,习惯到了看见一个数组就自然而然打出 for (...)
来。但是你有没有想过,很多时候,遍历数组其实跟索引并没有什么关系,代码只是要将数组里面的元素按顺序处理完。然而,数组天然就应该是顺序的,根本无需要一个额外的 i
来保证。换句话说,数组应该利用自身属性,提供无需索引的顺序读取方法,而索引只是在顺序读取的过程中的一个记录变量。
那这样有什么优势呢?
从处理流程上说,举个例子:假设你是一个接待员,工作是处理一列队伍的咨询。使用 for
的处理方法是:先计算整个队伍的长度,然后喊第一个人开始处理;每处理完一个人,就将序号加一再喊;直到序号等于队伍长度。而使用直接顺序读取的处理方法是:从队伍最前开始处理,每处理完一个,直接转到下一个重新开始处理,直到队伍没有下一个需要处理。这样以来就能省去了对队伍长度和索引的处理。
从代码逻辑上说,在数组上提供顺序读取方法,是将全局的语法转化为了相当于成员函数的执行,解除了和全局的耦合的同时,结合链式调用和灵活的回调函数能解放出极大的数组处理潜力。你能轻松在一行代码内基于数组进行非常多而灵活的处理,并且将「肮脏」的处理过程隐藏起来,直接得到一个处理得「连你阿妈都唔识」的结果,干净利落。
下面来认识一下这些顺序读取方法吧。
在 ES5(ES5.1) 中,JavaScript 新增了多个数组方法,包括:forEach, map, filter, reduce。
每个方法都接受一个回调函数作为参数传入,每个方法都会在取得一个元素的时候调用此回调函数,不同在于不同方法对待回调函数的结果上。
forEach 返回值为 undefined,适合通过数组来操作其他对象。
1 | arr.forEach(function callback(currentValue, index, array) { |
map 返回值为回调函数返回值组成的数组,适合处理数组变换。
1 | var new_array = arr.map(function callback(currentValue, index, array) { |
filter 返回值为回调函数返回真所对应的元素组成的数组,适合处理数组筛选。
1 | var new_array = arr.filter(function callback(currentValue, index, array) { |
reduce 返回值为初始值经过和每个元素作用后得到的最终值,适合遍历数组后得到一个值或者一个对象的情况。
1 | var new_array = arr.reduce(function callback(accumulator, currentValue, index, array) { |
别忘了在回调函数中返回结果!
配合几个编写代码时的常见场景,看看不使用 for
的解决方法。
给定一个数组,打印其元素。
1 | var arr = [1, 2, 3] |
for
1 | for (var i=0; i<arr.length; i++) { |
炫技
1 | for ( |
改写
1 | arr.forEach(el => console.log(el)) // 1 2 3 |
给定一个数组,将其元素都加一。
1 | var arr = [1, 2, 3] |
for
1 | for (var i=0; i<arr.length; i++) { |
炫技
1 | for ( |
改写
1 | arr.forEach((el, i, ar) => ar[i] = ar[i] + 1) |
更好
1 | const newArr = arr.map(el => el + 1) |
在允许的情况下尽量不要去修改原数据,而是返回一个新的数组。
给定一个数组,筛选出大于 2 的元素。
1 | var arr = [1, 2, 3] |
for
1 | var newArr = [] |
改写
1 | const newArr = arr.filter(el => el > 2) |
给定一个数组,要求使用其元素内容作为键,元素下表作为值,生成一个新数组
1 | var arr = ['a', 'b', 'c'] |
for
1 | var newArr = [] |
改写
1 | var newArr = arr.map(function(el, i) { |
ES2015
1 | const newArr = arr.map((el, i) => { return { [el]: i } }) |
给定一个数字数组,将其包含的数字累加
1 | var arr = [1, 2, 3] |
for
1 | var result = 0 |
改写
1 | const result = arr.reduce((ret, el) => ret + el, 0) |
给定一个键值数组,将其转换为一个对象
1 | var arr = [ |
for
1 | var result = {} |
改写
1 | const result = arr.reduce((obj, { key, value }) => { |
从以上例子中可以看到,ES2015 的代码更加清晰可读,而且代码打起来流畅省时(你自己试试!)。如果你还没使用上 ES6,那么应该赶紧去学!或许这篇文章和这篇文章能说服你。
也可以看看本人写的 《Understanding ECMAScript 6》笔记。
尽管数组新增的方法十分强大,但是 for
除了会在遍历数组中使用,还会在处理对象的时候使用,比如使用 for...in
遍历对象的属性(及其原型上的属性)。在这些场合上,就需要具体情况具体分析了。
给出 app 的版本以及版本的使用量,统计最新两个大版本的使用量。版本命名符合 semver 标准,形如 ‘x.x.x’。
1 | var apps = { |
for
1 | let largest = 0 |
ES2015
1 | let largest = 0 |
使用 for
和不使用 for
相比,相差不大,甚至代码看起来更清晰,而且有 ES2015 的加成,消除了变量泄露的影响。所以如果是遍历对象,就没必要去用数组的方法了。
截止目前位置(2017-10-17),从 benchmark 来看,在性能上,for
> forEach
> for...of
。
因此在一些对性能要求比较高的代码中,使用 forEach
和 for...of
需要谨慎,这有可能会成为性能瓶颈。另外 for...of
在浏览器上的支持度不高,所以还是可以暂时不使用,除非你清楚自己在干什么。
不过,仍然是那句话,代码首先是写给人看的,性能优化应该在功能实现之后再考虑。
一般来说,使用 for
和使用数组方法在功能实现上是一样的,但是由于 for
是编程语言层面的实现,可以使用 break
和 return
手段进行中断;上文中的数组方法由于是遍历调用函数,并不存在什么停止的条件,因此肯定是会将所有元素都过一遍。在这种情况下,就乖乖使用 for
吧。
当然也可以使用
Array.some
等方法模拟中断效果,但要是那样做还不如直接for
呢。
使用数组方法时,最容易出错的地方是和 Promise 一起使用的时候。
比如需要从不同的 URL 请求数据,极其容易写成以下错误的代码。
1 | const URLs = [ |
你会发现 results
的内容只是 Promise 实例,根本不是期望的值。代码的问题在于几乎所有的网络请求 API,返回的都是一个 Promise 实例。
正确的做法是使用 Promise.all
将多个 Promise 实例包装成一个 Promise 实例:
1 | const URLs = [ |
如果你使用 async/await,千万不要这样写:
1 | const URLs = [ |
同样这种写法只能得到一个 Promise 实例数组。应该这样:
1 | const URLs = [ |
⚠️ 注意你不能在没有
async
标识的函数中使用await
,因此在各种全局状态下是无法使用await
的。幸好 async/await 处理的就是 Promise,你只需要改用.then
就好了。
举一个更极端的例子,URL 请求需要按顺序发送,上一次的结果需要作为下一次请求的参数。怎么写呢?
Promise 方法:
1 | const URLs = [ |
async/await 方法:
1 | const URLs = [ |
但是看看用 for
会如何?
1 | const URLs = [ |
意外的简洁。这归功于数组的有序性,以及 for
在语言层面上的可被打断性。
可见在处理顺序的异步请求上,for
有着很大的优势,但在并发请求上,还是乖乖用 .map
吧(比 for
+ .push
要好)。
在那篇文章中,使用的是 Mocha/Chai/istanbul 和在线的 Codecov,以及和 Github 关系密切的 Travis CI,而且测试的 JavaScript 代码是 es5。现在 es2015 已经标准化了,那么教程也需要更新一下了。另外如果项目是私有项目,那么还是使用完备的离线测试环境比较好。接下来就是一个快速可行的教程。
测试框架无需变更,还是 Mocha + Chai 的组合,但是 istanbul 需要稍微变动一下。
如果你不需要 istanbul 做覆盖率测试,那么需要使用
npm install --save-dev babel-register
和mocha --require babel-register
使 mocha 能识别 es2015 的代码
使用新套件,直接安装 npm i -S mocha chai cross-env nyc babel-plugin-istanbul babel-register babel-preset-env
。
mocha 和 chai 不用解释了,nyc
可以理解是 istanbul 的命令行工具;babel-plugin-istanbul
是在 babel 中插入 istanbul,babel-register
是 istanbul 使用的 babel 接口,这样两个库就打通了;最后 babel-preset-env
是 babel 的运行配置。
先来配置 babel-plugin-istanbul
,新建 .babelrc
:
1 | { |
presets
配置告诉 babel 使用 babel-preset-env
。当然也可以用 ‘es2015’ + ‘stage-0’ 的组合,具体可以自行斟酌。
env.test.plugins
告诉 babel 在 NODE_ENV=test
的情况下使用插件 babel-plugin-istanbul
。
接下来配置 babel-register
,新建 .nycrc
:
.nycrc
是 nyc 的配置文件,和.babelrc
类似,当然配置也是可以直接写进package.json
的。
1 | { |
以上的配置直接用了官方的配置。require
字段告诉 nyc 使用 babel-register
,reporter
字段的 ‘lcov’ 会让 nyc 生成 lcov.info
文件和对应的 HTML 报告,如果使用 lcovonly
则只生成 ‘lcov.info’。’text-summary’ 则是会在控制台输出覆盖率等信息。
文件会默认生成在
/coverage
下,可以使用report-dir
字段指定。
最后,在 package.json
中加入:
1 | "scripts": { |
比如有 index.js
:
1 | export class Test { |
那么可以写 test/index.spec.js
,可以直接上 ES2015 的语法:
1 | import { expect } from 'chai' |
使用 npm test
运行测试,得到:
1 | > cross-env NODE_ENV=test nyc mocha test/**/*.spec.js |
在测试中经常需要测试 promise 等异步操作,虽然 Mocha 库是可以使用回调来完成测试的,但是我们当然要用 async/await 啦。
比如,需要测试 index.js
中的 requestAsync
:
1 | export class Test { |
那么需要先 npm i -S babel-polyfill babel-plugin-transform-async-to-generator
。
然后配置 .nycrc
,加上 ‘babel-polyfill’ 支持 generator 运行时:
1 | { |
配置 .babelrc
,加上 ‘transform-async-to-generator’,将 async 模式转换为 generator 模式。
1 | { |
接着这样测试异步:
1 | import { expect } from 'chai' |
在测试项 it
的第二个函数前加 ‘async’ 标志异步,然后在返回 promise 的调用前加上 ‘await’,OK。
1 | > cross-env NODE_ENV=test nyc mocha test/**/*.spec.js |
1 | // 填写内容让下面代码支持 a.name = “name1”; b.name = “name2”; |
【1】和【2】是填写的内容,【2】的答案是 prototype.name
,没争议。
问题是【1】,参考答案居然是 if(name){ this.name = name;}return this;
,这么随便地玩弄 this
不就是明摆着污染全局变量吗?暴力赋值不可取。
下面的一些高票讨论还说了一大堆解释的废话,连他自己都说自己好罗嗦。对,你不但罗嗦,而且还没有改错。注释里都说了给 window 的属性赋值,还不自知出问题,真是误人子弟。
先来分析一下题目,a 和 b 都从 obj 来,为什么同名的属性值不一样?可以看出,是对 obj 这个函数的调用方式不一样,a 是 obj 函数的调用结果,而 b 则是 obj 作为 构造函数 调用的结果。所以这题的重点应该是如何区分函数调用和构造函数调用。
一个关键字 new
决定了不同。new
的作用是什么呢?MDN 上说了,面试也会考你的,简单来说是三步,new foo
:
this
值会被绑定为 1 中的对象从 2 就可以看出 this
值会被 new
绑定为一个确定的对象,而不是像普通函数调用中那样自己不可预料,要看上下文的进程。
于是就可以在这里做文章。先来判断 this
的值。
1 | if (this instanceof obj) {} |
instanceof
会检查 this
的原型链上是否存在 foo.prototype
。也就是说能判断是否满足第 1 条,确保了对象能从 prototype
中读取到 name
属性。(毕竟代码中并没有给 b 的赋值中传入)
instanceof
并不是完美的判断方法,但是在这里足够了,后面会谈到这个问题。
1 | if (this instanceof obj) { |
非 new 调用的情况下,直接返回一个新对象就 OK 了。
而在 new 调用的情况下,可以看到 function obj(name)
定义的时候是有参数的,调用的时候却没参数,这就要小心了,为了安全起见,还是判断一下为妙。
1 | if (this instanceof obj) { |
一般来说,判断会写成 if (name)
,但是碰到 null
、0
、false
就 GG 了,所以还是谨慎点吧。
问题到这里就可以比较完美地解答了。
『instanceof
会检查 this
的原型链上是否存在 foo.prototype
』,为什么说得这么拗口,是因为需要表达出 instanceof
本来就不是真的用来检测是否调用 new
的方法。
在题目里面,要求的是 a 需要从原型链上读取到特定的属性值,所以 instanceof
的作用刚好在这里能符合要求而已。
函数调用除了题目中的方法还有第三种方法,那就是 foo.call
、foo.apply
,而且也能为函数指定 this
的值(所以还有 bind
)。因此是存在方法调戏 instanceof
的。
1 | foo.prototype.name = 'foo' |
这里的 foo
调用的方式是作为函数来调用,但是为 this
绑定的值是从 foo
上 new
出来的,换句话说,其原型链上存在 foo.prototype
,于是就骗过了 instanceof
。
于是 ES2015 来搭救你了,新增了一个 new.target
。于是修改成:
1 | if (new.target !== undefined) { |
项目是写一个 JavaScript 框架,干什么的在此并不是重点,但是首先需要一个可扩展的模块系统。最简单就是直接用 jQuery 扩展的写法,直接将函数等的挂载在一个对象下,不过如此一来模块之间依赖非常多的话,管理起来会十分困难。也可以使用 AMD / CMD 的模块化的方法,不过考虑到 ES2015 已经加入了 import / export 的语法,最好就直接使用。然而使用了 ES2015 的语法之后,仍然使用 AMD 等的语法就显得很别扭,但是又想要依赖注入功能怎么办?
解决方法是模(fu)仿(zhi)著名的 AngularJS 中关于依赖注入的源代码。
Angular 有两个版本,1.x 和 2.x,但是 2.x 中,淡化了模块的概念,直接采用 component 和 ES2015 的 import / export 的机制,所以依赖注入已经不太算是亮点了。而且 Angular2 采用 TypeScript 编写,从语法编写上也不适合作为参考。最后选定 1.4.5 版本。
Angular 项目下还有一个不怎么有人知道的
di.js
项目,是从 Angular 中独立出来的依赖注入库,但是从文档来看,也是需要 TypeScript 来使用。
文章较长,给个目录
JavaScript 如何实现依赖注入呢?AngularJS 给出了三个解决方法。
1 | // 直接在参数里面声明 |
实际上三个方式都是一样的,只是使用方式不一样,最后都是使用了 JavaScript 的闭包来实现依赖的注入,原理如下:
1 | function method ( $http ) { |
将依赖作为参数传入 menthod
得到的返回值就是是一个可以调用 $http
服务的函数了。
像这样 ['$http', function ( http ) {}]
最后一个元素是函数的结构可以称为一个‘可注入结构’。
Angular 库的源代码文件非常大,一般基本不会从头开始看。而 API 作为库对外的窗口,从 API 的使用顺藤摸瓜地查找代码是比较好的做法。Angular 模块的使用一般如下:
1 | // declare module |
先创建一个模块,用数组说明它的依赖模块,然后模块就可以调用 value
、service
、directive
等 API,API 的第一个参数是名字,第二个参数则是值或者函数或者数组,了解 AngularJS 的读者应该知道其实是值或者返回值的构造函数或者包含依赖和构造函数的数组。
打开 Angular.js ,查找出 module(name, requires, configFn)
函数的定义,位于一个更大的函数 setupModuleLoader
内。setupModuleLoader
为 module
函数编写了一些检测函数和变量。最重要的是 modules
变量,用来保存所有的模块信息。
下面来分析 module
函数。
1 | if (requires && modules.hasOwnProperty(name)) { |
可以看出如果模块重复创建是会覆盖之前的。
1 | /** @type {angular.Module} */ |
moduleInstance
就是将会返回出去的模块对象,可以看到里面有 name
和 requires
等属性和 provider
和 factory
等函数。
里面所有的方法,都是通过调用 invokeLater
和 invokeLaterAndSetModuleName
生成的新函数。新函数的上下文中带有 provider
和 method
信息。比如 service
函数:provider=’$provide’,method=’service’,暂时还看不出来信息有什么用,可以先跳过。新函数在调用的时候会将信息连同调用的参数一起 push 进模块的 _invokeQueue
属性中。
绕了一大圈,就是知道了:在调用 value
、service
、provider
这些基本的模块功能函数的时候,其实只是将构造函数和相关信息先保存了下来,根本就没有做初始化模块等工作。
但是作为一个库必定需要跟 window
或者 document
产生点关系不然无法操作 DOM,根据编写过不少库的经验来看,通常将这样的代码放在最后。于是拉到最后一看,gotcha。
1 | jqLite(document).ready(function() { |
明显意思就是在文档准备完毕的时候调用 angularInit
,转到 angularInit
的定义发现调用了 bootstrap(appElement, module ? [module] : [], config);
,再转到 bootstrap
的定义,在函数内部又会调用 doBootstrap
函数,一系列的检查之后,调用了 createInjector
函数就结束了,转到 createInjector
的定义一看,有 $provide
factory
等字样,说明找对地方了。
重点来分析 createInjector
函数。
函数体大概可以分成四段。
第一段是定义了 providerCache
、instanceCache
、providerInjector
和 instanceInjector
。最后返回 instanceInjector
对象。providerInjector
和 instanceInjector
各为将 providerCache
和 instanceCache
传入 createInternalInjector
函数的返回值。
第二段是 provider 函数的定义,用以供初始化时候的调用。
第三段是 loadModules
函数的定义,作用是,显然,初始化模块。
第四段是 createInternalInjector
函数的定义,函数返回的是真正的注入器。
从第一段的代码来看,真正的工作是在 loadModules
函数中,因为 createInternalInjector
函数只返回一个对象,没有 ‘side effect’ 的代码。
loadModules
函数上来就是一个对模块数组的遍历,然后在遍历内取模块的属性 _invokeQueue
来调用 runInvokeQueue
函数对已经缓存下来的对象或方法的构造函数进行处理。
值得注意的是在做调用 runInvokeQueue
前,有一个递归的调用 loadModules(moduleFn.requires)
,表明了在初始化本模块之前,会先初始化依赖的模块。
到目前为止,可以判明模块初始化的分两个阶段,第一个是:声明模块及其依赖模块 -> 缓存模块变量的构造函数;第二个是:选取一个根模块(对应 AngularJS 中的 ‘app’ 模块) -> 找出其依赖的模块,对于每一个依赖,递归地先初始化其依赖的模块,再初始化自身 -> 处理模块中缓存的变量的构造函数。
如此采取先缓存所有模块再通过依赖树来初始化的做法虽然看起来繁琐,但是得到一个重要的特性就是声明模块的时候不用关注依赖的顺序,只需要表明依赖就可以了。如果声明的时候就立刻初始化,则必须小心检查所依赖的模块初始化是否已经完成了,然而如此一来就退化成了普通的模块化方法了。延迟初始化是实现依赖注入的重要过程。
模块依赖已经明了,现在来看看作为处理函数的 runInvokeQueue
函数。
1 | function runInvokeQueue(queue) { |
重要的代码只有两行,provider = providerInjector.get(invokeArgs[0]);
和 provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
。
往上看一下,调用 providerInjector.get
相当于是调用 getService
。
1 | function getService(serviceName, caller) { |
代码虽多,但基本就是干一件事,返回 cache
中的对象,如果没有,就用 factory
创建一个再返回。而调用的对象 providerInjector
的定义来看,cache
就等于:
1 | providerCache = { |
OK,现在可以知道了那些被延迟初始化的模块元素会在这里被处理了。
从上文可以知道,invokeArgs[0]
的值为 $provider
,invokeArgs[1]
的值为 service / factory 等,invokeArgs[2]
则为参数数组。
看看以下的示例:
1 | // 如此使用 |
接下来就是分析模块元素(对外表现为 API)的代码了。
函数有点多,但是还是能看得出来。supportObject
不用管,只是负责转换一下参数,基本的函数是 provider
,factory
会调用它,然后 value
和 service
会调用 factory
。
1 | function provider(name, provider_) { |
第一个判断和第二个判断在 factory
调用的时候是无效的,因为 factory
调用 provider
的时候第二个参数是 Object,而且带有 $get
属性。实际上在本阶段做的是,就是将调用 API 传入的第二个参数(第一个参数是名字)再包装一层对象,再存储在 providerCache
中,对象统一拥有 $get
属性,或者说,接口。
其中,$get
属性是一个可供调用的函数,功能是即使模块元素混杂存储,也能被统一的接口成功调用。
对于 value
,调用 API 的时候传入的是值,因此需要包装成返回这个值的函数才赋值给 $get
。
对于 constant
,值是不变的,所以可以看到就直接存储了。
对于 decorator
,同样会定义 $get
属性。
对于 service
,设计上应该生成一个单例并存储下来。不过在这里,仍然是继续包装起来。
源代码:
1 | function enforceReturnValue(name, factory) { |
实际干了如下的事情:
1 | (function (name) { |
最后依然将包装好的函数存入 $provider
。
只是为什么还是存储在 $provider
,而不是直接调用函数进行初始化?比如 service
,为什么还要再包装上一层‘可注入结构’?
前文提到,使用延迟初始化实现了模块的依赖注入,使依赖的模块不需要提前定义。
实际上模块内的元素(factory / service 等)也是可以使用依赖注入的。使用过 AngularJS 的肯定知道定义某一个 controller 的时候可以注入某个 service,然而 controller 和 service 的定义顺序应该不能对代码运行造成影响。
因此,在此时,模块元素的“构造函数”(注意是用户自定义的那个函数而并非供
new 调用的那个函数)还并不具备运行的条件,因为还是需要等依赖的元素初始化。
于是某种意义上,模块元素就需要第二重注入。把‘可注入结构’缓存在 $provider
中实际上就是对应了前文叙述的‘把模块先全部缓存’,包装上一个函数再统一放在 $get
属性下明显是方便供下一阶段的调用。
万事俱备,只欠注入了。
分析到现阶段,大家应该对平常使用频繁的
service
、factory
等函数有了更深的认识了。
现在把精力放在 createInternalInjector
函数。
此函数在开始分析一节中已经提到了,作用只是返回一个对象。这个对象就是真正的注入器。
此函数被调用了两次,分别是得到 providerInjector
和 instanceInjector
。
注入器中重要的函数有三个,分别是 getService
、invoke
和 instantiate
。
在开始分析中已经大致介绍了,getService
函数干一件事,返回 cache
中的对象,如果没有,就用 factory
创建一个再返回。
对于 providerInjector
,factory
函数是:
1 | function(serviceName, caller) { |
不难理解,因为在调用 provider 的时候,providerCache
中的函数应该已经在上一个初始化模块阶段中被定义好,如果没找到,那么肯定是调用了未定义的 provider。
对于 instanceInjector
,factory
函数是:
1 | function(serviceName, caller) { |
instanceCache
本身就是空的,因此在找不到的时候,就去 providerInjector
里找 provider,然后得到其调用的结果,就是真正需要的实例(instance)了。$get
在这里就凸显出统一调用的用处了。
invoke
函数则是处理‘可注入结构’和调用函数。从源码中也可以看到组装参数和调用函数,其中也会调用 getService
去得到实参的值来实现注入。从这里的 getService
出发,又有可能调用 factory
继而继续调用 invoke
来得到所依赖的实例,直到没有任何依赖需要实例化,从而完美的实现了自洽。
instantiate
函数是用来处理 ‘service’ 的,是用来模拟 new
的,从代码来看也是如此:复制一个函数的 prototype,绑定为函数的 this
,然后调用函数。因此调用 service
API 的时候,可以完全使用构造函数的写法,同时也能得到注入特性。
读源码一般都会有一定的收获,或是技巧上的,或是思想上的。
当读完了 Angular 的依赖注入的代码后,才发现 Angular 虽然表明支持模块化,但是实际上所谓的模块化只是徒有其名,模块的定义只是方便框架自己做延迟初始化的工作,没有模块之实。模块只是依赖树上的节点,最终生成出来的命名空间跟模块没有一丁点的关系,所有模块里的东西,不论是 ‘value’、’service’ 和 ‘provider’ 等,都是平铺在 instanceCache
里面的。这样的做法明显的一个结果就是命名冲突,两个不同模块里面的同名对象,后实例化的会覆盖掉先实例化的。这一点非常的不好,因为完全不符合模块化的预期结果。
在下一篇编(fu)写(zhi)注入功能的时候,我会修改这部分使其能满足模块化的实际预期。
]]>PS: 有剧透才能更好地看电影。
PS: 如果你是想看好莱坞大片,还像和我同一片场的人那样带着爆米花和泡椒凤爪来看的,可以退票了。
简单来说,整部电影的剧情几乎都是在嗑了药的夏洛克进入多层梦境中发生的。他要解决上一季结尾中莫里亚蒂的 “回归” 的问题,于是在脑内——或者说是思维迷宫内——解决一百年前的另外一个 “死而复活的新娘” 的问题。而解决这个问题的方法,就是将自己带入到那个维多利亚时代中,思考自己会如何行动,剧情如何发展。这也就是为什么差不多通篇都是维多利亚时代风格的由来。案情重组,简而言之。不过,有一个问题,就是实际上根据设定,电影中福尔摩斯并不是十九世纪的人物,如此一来就对原来的案情进行了干涉,出现矛盾,这个后面再谈。
令来看福华 CP 的,卖腐的,相爱相杀的女性,啊不,观众大失所望,恐怕是悬疑色彩的浓重,剧情的多段跳跃和突然切换,以及似乎毫无卖腐的情节。然而我等从第一季就只对其具备推理性质和现代化改编产生强烈爱好的人却是大欢喜:这才是正剧的风格,第三季给某些群体派太多显而易见的糖了。
然而电影中的推理很好吗?电影没有派糖吗?非也。
先说推理。电影中的诡计归结成一句话就是:死了的新娘为何能复活并杀死其丈夫?答案是一开始就没死,是在姐妹们的帮助下演戏,之后再当街杀丈夫并给大众一种复活的假象再高明地自杀,不留下破绽,并且还继续利用这种牺牲换来了死而复生继而复活的恐怖现象来帮助姐妹完成对姐妹的丈夫的复仇。卧槽竟然吞枪自杀只是演戏?乍一看似乎有点敷衍,但是和朋友讨论后认为,如果考虑到十九世纪科技水平的低下和侦查手段的匮乏,加上法医的暗中帮助,实际上完全是可能发生的。所以说推理虽然并不出彩或者惊世骇俗,然而还是有意思的。只是编剧没有给解答过程一个酷炫的表现,所以观众就有不爽、硬了不射的憋屈。那为什么不表现得很光很亮很油还很 duang 呢?同样后面再谈。
再说派糖,电影中最明显的就是瀑布旁莫里亚蒂说的 “你们不如私奔吧” 的吧?原著可是福尔摩斯和教授一起掉下瀑布失踪的,电影里面华生来救福尔摩斯了啊。麦考夫在飞机里面对夏洛克嗑药的担心和毫不掩饰的 be there for you 你们无视了吗?午夜夏洛克和华生的深入交谈你们没触动吗?只能说糖派得有点晦涩了,伪粉抖一抖就掉了,真粉还是会粘着。
另外纵观整个案件,诡计的实施涉及到已经患绝症的新娘,女仆,为了证明自己和男性一样能胜任工作而不得不女扮男装的女法医(电影此处对 man 的翻译应该有问题)以及一众的女性秘密组织,换句话说,这其实标志着女性的反抗。电影中已有多处暗示:玛丽说她希望跟华生一样能做事,并且参加女性选举权的争取;麦考夫说有一个眼皮底下的 enemy,undetected, and unstoppable,还说 we will lose,because they are right we are wrong;玛丽从十九世纪的被晾在一边到二十世纪终于能和丈夫一起参与事情;华生家庭的情况;通通都是编剧支持女性争取权利的暗示。这也是跟随了近年电影频繁地使用女性作为主角,将女性塑造出不同形象的大流(我是从冰雪奇缘开始察觉近来电影有这样的趋势的)。这才是给女性派的最好的糖不是吗?
最后来说说电影对夏洛克这个人物的思考,顺便也把前面两个 “后面再谈” 解决了。原著中福尔摩斯基本上被塑造成理性思考和推理的机器,然而比较 tricky 的一点是根据设定,小说是道尔笔下的华生写的,小说中福尔摩斯的形象是道尔笔下的华生给大众塑造的,真实 (?) 的夏洛克到底是怎样的人无从得知。电影中也有反复地强调华生将夏洛克的案件写成文章发表。在午夜华生反复质问夏洛克关于感情和过去的问题,夏洛克没有说出来。这是编剧对夏洛克这个人物感情的探讨,没什么清晰的结论。之后没能保护受害人,夏洛克把自己关在房间里又磕药。然后莫里亚蒂就出现了在房间又吞枪了一次,没死,夏洛克觉得想不通怎么死不了的时候飞机着陆了把他震醒了。醒过来的二十一世纪是现实。一番交流后他又睡了,在梦里十九世纪醒来,开始了找玛丽和到在教堂里面解谜。最后指认凶手的时候他突然想到,凶手为什么要找他来破案呢?因为历史上新娘案就没被侦破,他作为侦探加入进案件了就产生了干涉,造成了奇怪的悖论。接着莫里亚蒂又出来了,场景就又切换到二十一世纪了,但是是更深一层梦里的二十一世纪,因为最后死尸活过来了,在现实里是不可能的。紧接着场景转到了著名的瀑布,这个估计就是夏洛克心里近乎最深层的地方了。可以看到,每当梦里有不寻常的事情发生,莫里亚蒂就会出来,试图让夏洛克偏离方向,只是第一次被飞机降落打断了。于是也就解释了为何解谜过程不酷炫,第一解谜发生在夏洛克脑内,显然不需要 “炫” 给自己看;第二,解谜会引出悖论从而引出教授,太酷炫会让观众大脑当机无法思考莫里亚蒂出现的原因。看到这你是不是忘记了这一段说的是夏洛克人物的探讨了?前面说了一大段剧情其实就是论证了在这个系列中,编剧认为莫里亚蒂就是夏洛克的心结和过去,或者说他脑中的魔鬼,会让夏洛克的思考出现问题。这是编剧对夏洛克过去的探讨。在原著中两人双双跌下瀑布,电影中是华生出来救场,并且表现得完全不像非梦境的华生。这 “夏洛克被心魔莫里亚蒂殴打,华生赶来将教授一脚踹下” 的场景表明夏洛克不再纠结莫里亚蒂 “复活” 了,而明白是他 “回来” 了。
于是正如新娘最终还是死了,莫里亚蒂大约的确已经死了。正如复仇是秘密组织干的,无责任猜测一下下一季的剧情大概就是教授的同伙来复仇了。
最后来看看编剧在电影里融合了什么呢?电视剧中的人物的重新运用,合格的推理,女性主义,对夏洛克的人物心理探讨,还有两季之间的承前启后。或许把这么多的东西融于一炉是有点用力过猛了,我刚看完的时候也是有点懵了,但是走回家的路上却越想越有意思。
我给这电影三个评价:
然而随着一道又一道的题目的通过,悲伤感越来越重却是怎么回事。
在平安夜和圣诞节一个人对着电脑敲代码来给虚拟妹子解锁装扮也是够自虐的。
前段时间看了《Understanding ECMAScript 6》,因为有 JavaScript 的基础,很快就上手了,还写了 笔记。然而编程只看书是不够的,还需要让身体熟悉起来。刚好最近在看「全部成为 F」这部新番,看到 ED 采用了「生命游戏」的表现形式,于是便有了用 ES6 来写一个的主意。
「生命游戏」的英文原文是「Game of Life」,是细胞自动机的一种形式,每个细胞的未来状态只取决于以其为中心周围八格细胞的当前状态。更详细的信息请看 wiki 条目,给出一个有意思的动画图。
而状态判断只有四条:
假设有一个棋盘,每一个格子代表一个细胞。在每一次生成下一代细胞,先遍历每一个细胞,查询它周围八格细胞的状态,设置本细胞下一代的状态。
显然这样的算法基本毫无意义,因为显然棋盘是不定大小的,细胞也不是每一代都一定会变化的,遍历整个棋盘也是浪费时间的。
实际上,发生变化或者有可能发生变化的细胞,基本是聚集在活细胞周围的。如果一个死细胞附近没有活细胞,那么这个细胞就不会发生变化。所以,可以换个思路,每一个曾经活过或者在活细胞周围的细胞都维持一个它的邻居细胞的数目记录。每当一个细胞活过来了,就通知周围八格的细胞,让它们的活邻居细胞的数目记录增加 1;相反每当一个细胞死了,就通知周围八格的细胞,让它们的活邻居细胞的数目记录减少 1。显然在更新完之后,周围八格的细胞不论生死都清楚自己周围的活细胞数,也就是能够得到自己的未来状态了。同时,在通知周围八个邻居的时候,也可以统计出对于本细胞来说的活邻居数,于是本细胞的未来状态也能够得到了。
于是算法能描述如下:
1 | 1) 在某一次生成本次状态中,有将改变状态的细胞集合 S |
ES6 中有 class 的概念,虽然实现方式其实就是 function 和原型,但是在写的时候就不用像以前用「模拟」的手段来编写啦。
基本来说,分三个主要对象:提供算法的 class Life,提供单元格绘制的 class Grid,提供 DOM 动画控制的 class Game。Game 从算法中得到需要重绘的单元格,通过 Grid 来绘制单元格。
已经有算法描述了,写起来并不复杂。新建一个 life.js
文件,导出 Life
类。
1 | export default class Life { |
构造函数只需要得到世界(棋盘)的长宽就行了,this.world
记录世界中受关注细胞的状态,this.changedState
记录将要改变状态的细胞。
算法本体代码,相当于描述 2) 中循环中的操作:
1 | _processLife ( x, y, state ) { |
2) 的循环其实就是得到下一代的状态:
1 | nextGeneration () { |
其他函数可以在 GiiHub 查看。
确定使用 HTML5
中的 Canvas
元素来绘制整个世界(棋盘),Canvas
元素的操作使用另一个类 C
,后面再写。
新建 grid.js
文件,导出 Grid
类。
1 | export default class Grid { |
构造函数要传入 canvas DOM 元素,棋盘的长宽,显示的选项和颜色选项。
绘制单元格的主要函数。
1 | drawCells( redrawCells ) { |
drawCells
函数是用来批量画细胞的函数,同样颜色的细胞放在一起画,就不需要频繁改变画笔的颜色。
drawCellAt
函数就是找到单元格的左上角距离 Canvas
元素左上角的距离,距离左边是第 x 个细胞宽度加细胞边框宽度,距离上边也是同样道理。
其中调用的 setPenColor
和 drawRect
还没有,于是就新增一个 c.js
文件,导出 C
类。其实就是 Canvas
元素的操作的封装而已。
1 | export default class C { |
不复杂,直接看代码吧。
1 | import Life from './life.js'; |
就是一些简单的动画控制方法,跟普通 JavaScript 写起来没什么不同。需要注意的是 enable
状态和 running
状态是不一样的,前者是指整个游戏的响应,后者是指动画的响应。
step
方法是迭代一步,run
方法就是用 setTimeout
来循环调用 step
了。在 run
方法中使用了箭头函数来隐含设定了 this
的值,ES6 的优势就体现出来了。
整个程序的主体是 Game 的实例,然而还是需要有人去创造一个实例出来,也就是说需要一个工厂函数。于是,新建 gol.js
文件,导出 GOL
类。里面写一个静态方法,用作创建 Game 实例的工厂方法。
1 | import Game from './game.js'; |
不过在 createGame
方法上就不要用 ES6 的语法了,因为方法是要在页面上调用的,目前还没有哪个浏览器完全支持 ES6。但是在方法里面用是没问题的,因为编译器会帮我们转换好。于是可以看到方法里面直接用 Object.assign( des, src )
的函数来合并参数,类似 jQuery 的 extends
函数。
到此还没完,回忆一下在写普通 JavaScript 库的时候,我们通常会直接包裹上一层适应各种环境的模块注册代码,本人最喜欢就是直接使用 UMD 了。
新建 boot.js
文件,执行非 ES6 形式的导出。
1 | import GOL from './gol.js'; |
OK,到此代码基本写好了,然而到在浏览器上执行还是有一段距离,主要是基本没有浏览器默认支持 ES6,我们还是需要将 ES6 的代码编译一下以便能放到浏览器上运行。比较有名的编译器就是 Babel 和 Google 的 Traceur 了。在编译的同时,还需要将所有文件打包成 bundle。
在进行了各种尝试之后(包括主流的 npm / browserify / jspm 等),最后发现使用 webpack
和 Babel
的结合是比较理想的。
先来把需要的东西都装上。
1 | npm i --save-dev webpack babel babel-core babel-loader babel-preset-es2015 |
个人其实非常讨厌安装到本地,明明都是可以全局安装的插件和工具。
而且每次开一个新的项目就要安装几十 MB 的重复东西实在无聊,npm 本身的树状依赖也是容易造成目录过深的情况。(据说新版 npm 有改善,但是不稳定)
个人的解决方法是固定一个开发目录,代码随便迁移。
webpack
我就不详细解释了。直接上 webpack.config.js
。
1 | module.exports = { |
目前来说,这样写就能让 Babel
编译 ES6 的代码的同时,也运用 webpack
自己的打包功能 根据 ES6 的模块语法 将文件都打包成一个 bundle。
打包出来的代码有点大,压缩一下,再写一个 webpack.config.min.js
。
1 | // webpack.config.min.js |
就能用 webpack
自带的压缩插件压缩代码了。
算法、绘图和动画控制都写好了,但是还不够,缺少了交互,还应该允许方便的自定义世界中的活细胞。比较好的交互方式就是允许通过在世界(棋盘)点击来放置活细胞或者死细胞。
于是考虑监听 Canvas
元素的 mousedown
、mousemove
和 mouseup
事件,做出类似画图那样的效果(每个细胞可以看成是一个像素点)。
先改造负责绘制的模块。
在 Grid
类中新增 drawAliveCellAt
、drawDeadCellAt
函数,负责独立绘制细胞。
1 | drawAliveCellAt( x, y ) { |
新增 on
、off
函数,负责绑定监听方法。
1 | on ( event, handler ) { |
新增 getXFromPixel
、getYFromPixel
函数,负责将像素点转换为单元格位置。
1 | getXFromPixel ( pixel ) { |
~~
是快速取整数。this.canvas.left
和 this.canvas.top
来自于类 C
的实例,因为鼠标点击事件取得的坐标点并非一定是相对于 Canvas
元素的左上角,还要减去 Canvas
元素的边框等。在 c.js
中将构造函数修改一下。
1 | constructor ( ele ) { |
类 Game
的修改有点复杂。先在类的构造函数中增加一个属性,负责记录鼠标状态。
1 | this._mouseState = { |
再增加三个方法。
1 | _onMouseDown ( e ) { |
鼠标按下,就在鼠标按下的位置改变细胞的状态,并记录鼠标状态为按下。接着如果鼠标弹起,那么就重置鼠标状态;如果鼠标移动并且状态是按下,那么就一直改变路过的细胞的状态。
_toggleCell
方法这样写:
1 | _toggleCell ( px, py ) { |
大概意思就是先将鼠标的位置转化为单元格位置,再反置此单元格细胞的状态。记录下 lastX
和 lastY
是为了不会循环反置,一定要有坐标变化才反置。
接下来就是将那三个函数绑定在事件上。新增 _setupLinsteners
函数。
1 | _setupLinsteners () { |
虽然使用了箭头函数优雅地绑定了 this
的值,但是这样写并不好,因为没办法解绑了,容易造成内存泄漏。改一下。
1 | _setupLinsteners () { |
通过将匿名函数的引用保存起来就能解绑了。
最后给个 demo 吧。或者玩玩 在线 demo
1 | <!DOCTYPE html> |
在线免费阅读:https://leanpub.com/understandinges6/read/
部分代码使用原书,代码版权归原书所有
块级{}中有效
同块级不可重复声明
没有变量提升
块级会形成暂时性死区(TDZ,Temporal Dead Zone)
基本和 let
相同,值不可修改
let
和const
最好不要在全局下使用
codePointAt
,双字节版的 charCodeAt
,得到字符 unicode
fromCodePoint
,双字节版的 fromCharCode
,从 unicode 得出字符
includes
,包含某字符串
startsWith
,以某字符串开始
endsWith
,以某字符串结束
repeat
,重复字符串
normalize
,unicode 正规化,举个例子:两个 unicode 字符合成一个
u
正则识别 unicode 字符
y
sticky,部分浏览器早就实现了
1 | let a = 1 |
1 | let a = 1 |
1 | function foo ( bar = 1 ) { |
1 | function foo ( bar, ...rest ) { // ✓ |
各种例子
1 | function doSomething() { |
避免了很多使用 new
的坑
1 | function Foo () { |
块级中可定义函数
this
, super
, arguments
和 new.target
的值都在定义函数时绑定而非运行时绑定
不可 new
不可改变 this
的值
没有 arguments
跟普通函数一样拥有 name 属性
1 | var foo = value => value; // input value, output value |
1 | // this 的绑定 |
1 | let foo = function ( s ) { |
1 | function foo ( text ) { |
1 | var foo = { |
对象的属性可以使用中括号 []
表示需要「被计算」,结果转换为字符串作为属性名使用。
1 | let a = function () {} |
和经典的 ===
几乎一样,区别在于:
1 | console.log( +0 === -0); // true |
1 | Object.assign( target, ...source ) |
读取源对象可列举的、自身的属性,将其赋值到目标对象上,覆盖旧属性,并非通常意义的复制。
此小节查询 MDN 后补充上
使用 Object.getOwnPropertyDescriptor(source, key)
读取,使用 Object.defineProperties
定义。
属性以最后一个定义的值为准
Object.getPrototypeOf
,得到原型
Object.setPrototypeOf
,设置原型
用以访问对象的 prototype
1 | var {a, b: { c, d }} = c |
解构可以有默认值,但只会在需要的时候求值。
2ality 有更详细清晰的解释:
1 | let {prop: y=someFunc()} = someValue; |
1 | var foo = Symbol() |
Symbol( 'description' )
生成局部 Symbol,即使 description
相同生成的 Symbol 也不一样
Symbol.for( 'description' )
生成全局 Symbol,description
相同则 Symbol 相同
Object.getOwnPropertySymbols( object )
原书本节未完成
原书本节大部分未完成
生成迭代器的函数
1 | function *createIterator() { |
数组、字符串、映射(Map)、集合(Set)和元素数组(NodeList)都可迭代(iterable),可使用 for-of 语法
Symbol.iterator 指向得到迭代器的函数
1 | let values = [1, 2, 3]; |
1 | let collection = { |
ertries()
,返回键值对迭代器
keys()
,返回键迭代器
values()
,返回值迭代器
通过 []
的访问是 code unit 方式
通过迭代器则是字符方式(几乎是,某些 unicode 支持不足)
返回的是数组中的单个元素
1 | function *foo () { |
1 | function *createIterator() { |
使用 yield *
1 | function *createNumberIterator() { |
可以 yield *"string"
,会调用字符串的默认迭代器
1 | function *foo () { |
以下是书中的例子,写得并不好,变量 task
的管理容易出问题:
1 | var fs = require("fs"); |
1 | class foo { |
类的属性最好都在构造函数里面创建。
类声明本质上就是以前的函数声明,除了以下有所不同:
strict mode
运行Object.defineProperty()
new
会抛异常const
定义的,对于外部则不是)1 | let foo = class {} |
1 | let foo = class foo2 {} // foo === foo2 |
匿名类作为参数
1 | function createFoo ( c ) { |
立即调用类表达式(有点像立即调用函数表达式)
1 | let foo = new class { |
1 | class foo { |
1 | class foo { |
静态成员同样不可列举
比起 ECMAScript5,ECMAScript6 的派生方便了很多
1 | class Rectangle { |
在派生类的构造函数中,调用 super
是必须的。如果连构造函数都没有,则:
1 | class Square extends Rectangle { |
- 只能在派生类中用
super()
- 使用
this
前必先调用super()
来初始化this
- 只有在构造函数返回一个对象的时候才可以不用
super()
覆盖、隐藏父类方法
1 | class Square extends Rectangle { |
仍然可以使用 super
调用父类方法
1 | class Square extends Rectangle { |
类方法没有 [[Construct]]
这个内部方法,所以不能被 new
。(什么是[[Construct]]
)
相当于 ES5 中定义在构造函数上的方法(注意不是定义在构造函数的原型上),派生类显然也能调用
除了 null
和生成器函数外
1 | // 使用函数 |
能够得知类的调用状态,应用例如:阻止抽象类被实例化
1 | class Shape { |
Promise 是老朋友了,所以没有什么好记录的,就记一下语法。
1 | let p1 = new Promise( function ( resolve, reject ) { |
注:本章的代码似乎有一些问题,基本参考 MDN 为准
this
的值为 undefined
直接使用原书代码:
1 | // 导出数据 |
default
语法,否则函数和类都不能使用匿名export
只能用在顶层中as
和 default
语法的情况,给出一个来自 2ality 的表格
Statement | Local name | Export name |
---|---|---|
export {v as x}; | ‘v’ | ‘x’ |
export default function f() {} | ‘f’ | ‘default’ |
export default function () {} | ‘default‘ | ‘default’ |
export default 123; | ‘default‘ | ‘default’ |
可以看出,所谓的默认导出其实就是用了 default
作为名字罢了。
还能够将其他模块重新导出
Statement | Module | Import name | Export name |
---|---|---|---|
export {v} from ‘mod’; | ‘mod’ | ‘v’ | ‘v’ |
export {v as x} from ‘mod’; | ‘mod’ | ‘v’ | ‘x’ |
export * from ‘mod’; | ‘mod’ | ‘*’ | null |
导入有很多方法,基本使用到的其实只有几种,以下来自 MDN:
1 | import name from "module-name"; |
最后那种导入是相当于将代码执行了一次。通常可以用来做 polyfills
和 shims
。
Number.isInteger
,判断整数
Number.isSafeInteger
,判断是否是有效整数
Math 中加入很多函数,例如双曲正弦、双曲余弦之类的
]]>a feature-rich JavaScript test framework running on Node.js and the browser
a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework
Free continuous integration platform for GitHub projects
Continuous Code Coverage
简单来说,就是使用 mocha 作为测试框架,chai 作为断言库,将项目交给 Travis CI 做自动测试,交给 Codecov 做覆盖率测试。
以我自己的项目 simpleTemplate.js 为例。
先给项目装上 mocha 和 chai。
1 | npm i mocha chai --save-dev |
在 package.json
文件中添加测试脚本命令行。
1 | "scripts": { |
项目根目录新建文件 test.js
。
引入 chai 以及三个要测试的库。
1 | var expect = require( 'chai' ).expect; |
定义必需的数据。
1 | var testData = { |
mocha 用起来其实也不复杂,常用的就是使用 describe
定义一个项目,使用 it
来执行一项测试。
1 | describe( 'bare', function () { |
这里就是定义了一个 bare
项,里面再定义一个 string template
项,然后在 it
的回调函数中写断言,第一个参数可以写上断言描述。如果断言失败,测试就会失败。
OK,执行 npm test
,可以看到结果输出。
1 | bare |
expect(...).to.equal(...)
就是用到了 chai 了。
剩下的测试编写就不再详述了,基本都一样。
去 Travis-CI 官网使用 github 帐号登录,开启对应项目的访问权限。
然后在 package.json
同目录(根目录)下,新建文件 .travis.yml
,写入如下内容。
1 | language: node_js |
git push
一次,再访问 Travis-CI,会发现已经给你显示出测试结果了。
那么好了,测试通过,何不贴个奖章 show off 一下呢?
在项目旁边有一个黑色加绿色的按钮,点一下,弹框中选择 markdown 格式,将代码贴进 readme
,再 git push
,去 github 的项目页一看,是不是高大上起来了呢?
代码覆盖率其实也没必要到 100%,只要不是太低的值就可以了。
去 Codecov 官网使用 github 帐号登录,开启对应项目的访问权限。
然后在 package.json
同目录(根目录)下的 .travis.yml
加入如下内容。
1 | env: |
your-uuid 替换成开启项目时生成的 Repository Upload Token
。
继续 git push
一次,再访问 Codecov 就可以看到项目的代码覆盖率了。
照样里添加上 badge,点击右边齿轮的 badge
,就可以得到 markdown 代码了。
以后每次 push
,都会自动运行测试和代码率覆盖统计,去查看一下就知道代码有没有错误或者改进了。
参考文章:
Basic Front End Testing With Mocha & Chai
]]>良好的文件组织结构不仅能帮助我们更快地定位文件,更能配合开发工具形成流畅的开发流程,从而提高编程效率。
以下的目录和文件都放在存放应用的根目录 app
下。
Electron 应用的配置文件,经常做 node 开发的人应该很熟悉了。稍微说明一下一些字段:
name
: 应用的名字,本项目就是 radioit 了
description
: 应用的描述
version
: 应用的版本号
author
: 作者名字
email
: 作者的邮箱
Electron 应用的入口点,可以在 package.json 的 main
字段自定义
node 库的目录,一般不用手动管理,而是使用 npm 来安装和卸载库。
存放 node 模块的目录。
存放源代码的目录。
存放待编译的 css 代码,比如本项目用的 .styl 文件。
存放浏览器端的 javascript 源代码。
因为使用 AngularJS,所以此目录的结构就照搬 AngularJS 项目的结构。
通常来说有两种:按 service / controller / directive 分目录存放,按功能模块存放。
本项目选择按功能模块存放。
供 browserify 打包的入口点。最终浏览器端的 javascript 代码会打包成一个名为 bundle.js
的文件。
存放编译好的 CSS 文件。
存放字体文件。因为 Electron 可以访问本地文件,所以自定义字体也基本不需要考虑网络传输问题。
存放图片文件。
存放客户端的 javascript 库,比如 jQuery,underscore,AngularJS 等。
browserify 编译 javascript 代码后输出的文件。
存放 HTML 模板文件或者包含 HTML 代码的文件。
有关 node 的开发,跟普通的项目并没有什么两样,需要什么库就直接使用 npm 安装,然后再代码中使用 require
就可以了。
然而虽然 Electron 为 webkit 内核提供了 io.js
的运行环境,但是最好还是避免在客户端(浏览器)的 javascript 代码内混杂需要 node 依赖的代码。换句话说,最好将需要 node 依赖的 javascript 代码和平常在网页中使用的 javascript 代码分开。这样做的好处是不会搞混相关的 API 和设计模式,毕竟 node 大部分时候都是用在服务端上的。
本项目将 node 相关的代码放在 lib/
目录下,负责应用的业务逻辑,其既有可能被主进程所用,也有可能被渲染进程所用。node 相关的代码不需要编译合并。编写时在目录下新建 xxx.js
文件,写好需要 exports 的内容,在其他文件中则使用 require( './xxx.js' )
就可以了。
因为界面的渲染采用 webkit 引擎,所以 javascript 的编写和网页开发没有分别。
在 package.json
的 scripts
字段中增加一条命令:
1 | "build:js": "browserify src/modules/entry.js -o static/js/bundle.js" |
然后在编写好 javascript 代码的时候,执行 npm run build:js
进行编译。
CSS 的编译则是加入如下:
1 | "build:css": "stylus -u nib src/css/app.styl -o static/css/app.css" |
最后需要运行应用来测试,增加命令:
1 | "test": "electron main.js 2>&1 | silence-chromium", |
需要测试的时候使用 npm run test
,需要运行则使用 npm run start
进行重新编译和运行。
由于某些原因,我需要去爬获取一些国家旅游景点的信息。
找到国家旅游局的网站,然后找到一个 5A 风景区目录。
于是去 pyspider 的 demo 页新建一个项目:5stat,就去爬了。
网页比较特殊,看起来是用 dotnet 写的,翻页是按钮调用 js 代码实现的。跳转后还是同一个网址。
这里就要用到 pyspider 支持的页面载入后运行 js 脚本的功能。
先分析翻页按钮干了什么。
如下图,调用一个名为 __doPostBack
的函数。
在页面上寻找这个函数,看到函数体如下:
1 | var theForm = document.forms['form1']; |
函数将 theForm
里面的 __EVENTTARGET
值设置为 PageNavigator1$LnkBtnNext
之后就提交了。
找到 theForm
对应的元素,看见有三个隐藏域, __EVENTTARGET
、__EVENTARGUMENT
和 __VIEWSTATE
。
附近还有一个隐藏域 __EVENTVALIDATION
。看名字就觉得要提交。
于是试试只提交这三个值看看会不会报错。
在 chrome 上安装 postman 这个应用,打开。
修改方式为 POST,填上地址和三个域的值,send。
OK,返回了正确的页面,也就是可行了。
嗯 pyspider 的爬虫脚本怎么写就不详述了,不会的看文档。
着重列出爬虫执行的 js 脚本的功能。
1 | function() { |
如此一来在回到爬虫脚本中的时候就能得到下一页跳转的参数了。
因为 pyspider 的文档说明对于每个项目内的相同网址会忽略,于是按照教学提示给网址加了个 #
。很明显这样的网址不会改变请求的参数(使用一些其他技术的情况下除外)。
之后不再使用这个方法,因为 pyspider 判断是否同网址实质上是简单地将网址 md5 一下生成任务 id,以此来判断是否同一个爬虫任务。后来用的方法是直接重写任务 id 的生成。
然而在爬下来的数据中却发现有除了旅游地点外的酒店信息。
原来同一个页面也有五星级饭店的信息。如下图,注意最后有一个 #
。
点击后跳转到一个网址:http://www.cnta.gov.cn:8000/Forms/TravelCatalog/TravelCatalogList.aspx?catalogType=hotel&resultType=%u4E94 的页面。
看起来跟旅游地点差不多,新建一个项目 5hotel,直接复制粘贴之前的代码,就是改了一下网址。
期间还将任务 id 的生成重写了一下,这样即使请求同一个网址也没问题了。
然而运行的结果却失败了。
在 content 中很明显看出页面获取不全,然而代码是直接复制的,页面也是相同结构的,为什么会出现这个问题呢?
然后我就被困扰了两天,接着就没在去管,盘算以后自己实现个爬虫再爬好了。
今天我再上去看,爬 5A 风景区的项目一直稳定运行。
五星级饭店的却还是无法抓取全部页面。
然后我鬼使神差地给网址加了一个 #
。网址从:
www.cnta.gov.cn:8000/Forms/TravelCatalog/TravelCatalogList.aspx?catalogType=hotel&resultType=%u4E94
变成
www.cnta.gov.cn:8000/Forms/TravelCatalog/TravelCatalogList.aspx#?catalogType=hotel&resultType=%u4E94
然后就能爬了!!!
shenmegui???!!!
我也搞不清楚究竟是 pyspider 的问题还是 phantomjs 的问题还是 dotnet 的问题了。
]]>python 脚本的详细编写,请看之前的博文:radioit 计划——动画广播辅助脚本 radioitScript。
需要用 node 实现脚本中的某些逻辑是获取和提取广播的信息,整合成 JSON 格式的数据。
而用一些库就能轻松做到。
superagent 是一个极其简单的 AJAX 库。
使用方法简单得令人发指。
1 | var request = require( 'superagent' ); |
还用介绍吗?不用了。
bluebird 是一个 Promise 库。
凡是类似 IO 的操作,必定需要异步。经典的解决方法是回调,然而是时候用 Promise 了!
bluebird 声称拥有无与伦比的速度。其实更实用的功能是它支持能够将一些本身是不支持 Promise 的库转化为支持 Promise 的库。
然而,要配合之前的 superagent,则需要另外一个库 superagent-bluebird-promise。superagent 本身不支持 Promise,从上面的代码来看就是使用回调的方法,这个库就是将 superagent 和 bluebird 融合在一起的“融合卡”。
使用的时候只需要:
1 | var Promise = require( 'bluebird' ); |
立刻就可以使用上 then
了,方便吧。
cheerio 是一个语法类似 jQuery,为服务端提供 jQuery 核心功能的库。这里用到的是它的 CSS 选择器功能。
代码同样很简单,使用过 jQuery 的人会倍感亲切。
1 | var cheerio = require( 'cheerio' ), |
使用 cheerio 有比较推荐的做法就是添加上 decodeEntities
和 lowerCaseAttributeNames
这个两个 options 配置,能避免各种 HTML 文本的奇怪问题。
1 | $ = cheerio.load( HTMLtext, { |
综上,四个库的混合使用例子如下:
1 | var Promise = require( 'bluebird' ); |
npm 安装库的过程略。
因为是信息整合,那么必定需要有一个统一的数据格式。于是先来确定数据格式。
广播站中所有广播的信息整合数据格式。
1 | // data will be formated as a json object in following structure: |
单个广播的信息整合数据格式。
1 | // data will be formated as a json object in following structure: |
有了输出的数据格式,抓取信息的时候就能有的放失。
以 響 - HiBiKi Radio Station - 为例。因为在之前编写脚本的时候已经得到了页面上信息的位置,所以可以直接应用在代码中。
1 | // 一些固定的信息和变量 |
以下开始获取广播站的广播。
1 | // 获取信息的对象 |
以下开始获取某个广播的详细信息,函数定义在上面的对象中。
1 | getBangumiAsync: function ( id ) { |
代码看似很多,其实就是多了信息提取的部分,其他代码完全就是上一节中四个库的混合使用。
要注意的有一点,就是 promise 链中的 then( fulfilledHandler, rejectedHandler )
。其中 fulfilledHandler
在最后需要使用 return data;
将数据传出去,而 rejectedHandler
也需要使用 throw new Error( err );
重新抛出错误,不然 promise 链中下一个函数将不会得到处理好的数据或者异常(因为已经处理掉了)。
最后别忘了将对象导出。
1 | module.exports = hibiki; |
同理,另外两个广播站的代码基本都一样,不同的只是信息提取的部分。
对于取数据的调用者而言,是无需理会数据从哪来的,只需要知道使用什么 API 就够了。
再者,既然有 “整合” 之名,就要行 “整合” 之实。因此要将这三个或者日后出现的更多个广播站提取代码整合起来,只提供一个调用入口。
新建目录 provider
,将三个广播站的脚本都放进去。
再新建一个 provider.js
文件,写入以下代码。
1 | var catalogue = { |
整体思路是提供一个可调用的列表,然后根据参数调用相应脚本的功能,就是一个 dispatcher
的功能。
如此,就实现了应用的一大部分主要功能了。
]]>应用 github 地址。github 代码和文章代码并不同步,用作预览和 PR。
一句话概述:开发的应用是一个抓取网页有用信息并重新统一排布的应用,是 之前文章 提到的 radioit 计划里脚本的 GUI 版本。
关键词:网页抓取、信息统一、信息排布、脚本的 GUI 版本
功能:
业务流程:
技术联想:
技术要点:
脑内讨论
A:Electron 有意思地使用了 main
进程和 render
进程,render
进程产生于 main
进程中,因此可以简单地产生多个 render
进程,也就是多窗口。这是一个优势。
Q:不用 angular,用 react 是否可以?
A:可以,然而在假定选用了 react 之后,然后脑内模拟了一下编程的过程,react 似乎并不适合 html 代码经常修改的场合。而自己比较在行的是写 html 和 css,在界面设计上必定经常修改。另外在 material design 的 UI 框架上,使用配合 angular 的 angular material 显然更具操作性。当然 react 下也有 material design 的 UI 框架,但在试用之后感觉不太好用。另外就是自己翻译过一篇很长的有关 angular 的文章,对 angular 比较熟悉。日后考虑改用 Polymer 重写。
Q:material design 是必须使用的吗?
A:作为桌面应用,需要有一点时刻记住的是桌面应用跟网页是不一样的。桌面应用需要稳定的窗口,要有标题栏等清晰的组件,也不需要太花哨的特效。material design 或者受 material design 影响的一些简洁 UI 风格已经在某些桌面软件上应用开来。Electron 作为使用网页作为 GUI 表现,使用 material design 是个稳妥之举。
Q:为什么不用 SASS / LESS?
A:SASS 需要 Ruby,对非 Rubyer 是非常无理的要求,逻辑表现能力强大而无用(非常用);LESS 语法简单,支持混写,但逻辑表现力太弱。stylus 则是既有强有力的特性,也足够简单。有时,工具够用就行。参考:Why I Choose Stylus (And You Should Too)
Q:node 和页面中的 angular 如何沟通?
A:main
进程和 reander
进程有特定的模块进行通信。render
进程能通过页面中的全局变量和 angular 进行通讯。
Q:为什么要使用 node 的库来处理网页请求和内容提取?angular 自带有 $http 不是更方便?
node.js 的安装是必须的,不多介绍。安装完自带 npm 管理工具。
用的最多的 node 命令:1
2
3
4npm i xxx -g
npm i xxx --save
npm u xxx --save
npm update
第一条是全局安装 node 模块。比如一些常用工具,每一个项目都可以用到的工具等。这些模块可以写在 package.json
中的 devDependencies
字段中。
第二条是本地安装 node 模块并保存信息到 package.json
中。适合项目特定使用的模块。这些模块可以写在 package.json
中的 dependencies
字段中。
第三条是卸载本地安装的 node 模块。node 模块太多了,尝试多个选最好的。
第四条是升级 node 模块。
以下是 package.json
文件的暂时内容。
1 | // package.json |
暂时并没有太多的东西,注意要开发基于 Electron 应用,electron-packager
和 electron-prebuilt
必不可少,一个是 Electron 的打包工具,一个是 Electron 运行环境。而 silence-chromium
则是将 chromium 控制台信息输出到系统终端的工具。其他的工具都是博主开发过程中精选过的工具,还请读者自行 Google 之来学习。
如果你看过本博客之前的一篇文章:i18n.js 库的编写兼使用 npm 辅助开发,就知道博主是能用 npm 就不用 gulp / grunt 的,因此在 scripts
字段中也写上了运行的脚本。
angular 的版本比较稳定,因此直接用 bower
来获取,不推荐其他包管理工具。
bower
需要先使用npm install bower -g
来安装,也需要配置了 git 的环境。如果你使用 github for windows,那么请使用 gitshell 来运行。
angular 的安装在下一节中。
对于 bower
来说,angular material 跟 angular 是一样的东西,只是后者是前者的依赖。
运行 bower install angular-material
, bower 会自动将依赖的的 angular
、angular-aria
和 angular-animate
一并安装上。
安装完后所有文件会在项目目录下的 bower_components
中找到。
具体参考:Quick Start
在 package.json
中有一个 main
字段,值是 main.js
。这个就指定了 Electron 启动应用的入口。
准备好文件结构。
1 | app/ |
1 | var BrowserWindow = require( 'browser-window' ); // Module to create native browser window. |
代码好像很多,其实基本就是照抄 quick start,没有任何压力。
博主写的 main.js
和 quick start 中的有所不同。在新建 mainWindow
的时候,加入了其他参数 show: false
和 resizable: false
,分别是隐藏窗口和窗口不可拉伸。也增加了一个:
1 | mainWindow.webContents.on( 'did-finish-load', function () { |
作用是网页内容完全载入后才显示窗口,避免一些内容还没载入完就显示。
最后运行 npm run test
看看结果。
]]>有什么问题请留言。
Electron 是什么?它之前的名字是 Atom Shell,是 Github 开发的结合了 io.js 和 chromium 的跨平台桌面应用框架。Github 自己出的编辑器 Atom 以及微软出的编辑器 VSCode 都是基于这个框架。
众所周知,Google chrome 就是基于 chromium 而发展出来的一款优秀的浏览器。因其出色的体验和网页解析性能,所有国内出产的 < del > 山寨 </del > 浏览器 / 双核浏览器,无不选用了 chrome 作为内核。所以在网页解析渲染方面,使用 chromium 是极其正确的选择。
那跟平常的桌面应用构建,使用 Electron 又有什么优势呢?
普通的桌面应用构建,比较成熟的语言不外乎 C/C++、Java、C#、Python 等。然而 C/C++ 易学难精,即使其 GUI 框架有 MFC、Qt、KDE 等众,也是极难快速开发;Java 的 GUI 烂得不提也罢;C# 极有可能成为以后霸主,然而还在跨平台表现上有所欠缺;Python 则是个人喜好关系顺带一提,其实很少作为 GUI 主力语言被使用。(当然你可以阅读本人的另一个 有关 python 和 Qt 构建桌面应用的系列)
Electron 则是使用了 Javascript 作为主力语言,并且为其加上了原生支持 html5 和 CSS3 的浏览器。从 GUI 构建来说,使用 html 和 css 的网页构建显然更加简单,成熟的工具和技术数不胜数;而作为桌面应用着重依赖的 IO、进程和网络通信模块等则由支持 ES6 的 io.js 提供,这样前端后端均采用 Javascript 语言,大大降低技术复杂性。
如果你有经常关注前端的消息,那么一定听说过一个国人开发的 GUI 框架:node-webkit。然后一看到 Electron,就会皱皱眉头:这不就是 node-webkit 嘛!
然而,Electron 和 node-webkit 并不一样,其 github 项目上有详细的对比,链接。
就个人理解来说,NW.js 偏向网页主导,是一个加上了 node.js 的浏览器;Electron 则是 javascript 主导,是 io.js 加上了一个 chromium。
准确来说,Electron 只是选择了网页作为 GUI,并非为 GUI 绑定了 javascript。在 Electron 文档的 Quick start 中很明确地指出「It doesn’t mean Electron is a JavaScript binding to GUI libraries. Instead, Electron uses web pages as its GUI, so you could also see it as a minimal Chromium browser, controlled by JavaScript.」
在听说了 node-webkit 之后,我曾经上手把玩了一下,当时也是惊讶于其结合了浏览器内核而得到的强大表现力。因为自己在前端方面有一点技术,所以在编写界面的过程中感觉非常舒服。不过我也留意到其在软件方面的能力明显有比较大的欠缺,除了能读写文件外似乎没有什么亮点。(当然不排除在改名为 NW.js 后会加入了更多功能的可能性)
总之,NW.js 更像是将网页打包成应用,而 Electron 则是实际开发的应用。
如果要将网页设计应用到软件界面开发上,那么一些 MVC 框架或 UI 框架就比较适合。MVC 框架中比较有名的是 knockout 和 Backbone,而 UI 框架,则是 reactjs、angularjs 和 polymer 最为著名。国产的还有 avalon。
那么为什么选 angular 呢?因为 angular 的理念比较符合开发网页应用,更重要的是有 angular material 这样一个比较能使用的 UI 主题。相比之下,knockout 和 Backbone 功能太弱,reactjs 则是太激进(一开始我是选 reactjs 的,但是一番尝试之后还是放弃了),polymer 则未作深入了解。
不过,就像 Electron 只是选用了网页作为呈现 GUI 的方式,那么在编写基于 Electron 的应用的时候,GUI 框架的选择其实并非固定死的,如有必要或者个人喜好,转而使用 polymer 或者 reactjs 也未尝不可。
如果有看过鄙人写的 python × Qt 应用开发系列,那么一定知道本人的教程都偏向实践,喜欢实际解释代码和一定程度地搞清楚技术的细枝末节,而非跟着网上一搜一大把的英文教程或者官方文档演示一篇后以近乎翻译一般地写出所谓的 “教程”。官方文档就摆在那,谁不会 RTFM?
在本系列中,鄙人同样会以记录一个应用的开发流程的形式来呈现成功(或者说,可行)的开发方式。有时会有大量的代码,有时又会有长篇的理论讨论,有时又会有大段的思维解释,希望读者能耐心读下去。
]]>python × Qt 应用开发 · 2.5 — 改进软件界面
]]>写JS库也不是一两次了,当然只是小型或者微型的。不过思想和方法和大型库都是通用的。一般是直接在sublime text里打开一个JS文件,然后写下一个Self-Executing Anonymous Functions(自执行匿名函数?),接着在函数里面创造库的对象,最后将对象挂在window
对象下。
Show you the code的话就是以下所示:
1 | (function( window, undefined ){ |
学得这样的写法是来源于对jQuery源代码的阅读。
通过将代码都包在一个匿名函数中,实现了一个闭包。如此一来在闭包内随便折腾,也不会污染到外部全局环境(当然是在编写可靠的代码的情况下)。
不过,随着AMD和CommonJS标准的流行开来,越来越多JS库都将自己模块化。过程也不复杂,只要遵循一定的规则就可以了。
而对于编写一个简单的JS库,将github上UMD项目给出的模板修改一下就OK。
修改后代码:
1 | ;(function( root, name, definition ) { |
注意最后不再需要手动将库挂载在window
对象下,而是只是返回对象。挂载方式已经转交给外部函数判断。
很久的以前,我曾经写过一个jQuery插件,功能是为表格添加分页和异步载入。然而在写之前并没有清晰地定下整个插件的功能和限制,导致最后写出来的插件身兼数职,连表格美化与自定义CSS等也做了进去。加进去的功能有可能只是随手实现的,也许并不适合此插件管辖,造成了“做得不好非要做”的尴尬。
另外,功能的繁琐与代码段的反复抽象提取导致了代码的凌乱不堪,进而导致测试出bug的时候完全搞不清楚问题所在。
最后代码膨胀到完全不能控制,自己写出来的代码连自己都不敢修改。
在写jQuery插件的时候,十分容易变成了写“使用jQuery的代码集合”,缺少性能和架构的考虑。这跟jQuery本身十分强大和灵活的特性有很大的关系。
写库或插件,目的应该是将通用或者复杂的逻辑实现封装起来,通过提供简洁的API来实现功能的调用。
先将手从键盘上收回,拿出纸笔,好好列出对JS库的描述。
思路是不是清晰了很多呢?可以看到核心逻辑就是一个有访问函数的DFS或BFS算法。
近年的前端大发展,也催生了很多自动化工具。node的流行更是让很多软件管理和后端开发的思想能应用到前端开发上。
经典的前端开发不外乎就是写HTML、写CSS、写Javascript,然而在前端代码量越来越大的现在,一个自动化的构建工具则能大大提高工作效率。
如果Google一下前端构建工具,那么基本就是Grunt和Gulp。
本质上,Grunt和Gulp都是任务运行器,尝试将前端的代码生成甚至发布统合到几个甚至一个命令行中。它们本身作为npm的一个模块,并没有什么作用,真正做事的是以其为平台的大量插件。通过将各种各样的插件整合起来,Grunt和Gulp就能实现自动化的任务执行。
但是慢着,以前不是很流行什么网页三剑客的吗?甚至用DreamWrear就能做网页啊。任务运行器、插件什么的是个什么鬼?!
是这样的,现在的前端开发,虽然最终结果还是写HTML、写CSS、写Javascript,但是过程却已经变化多端,内容也逐渐丰富。
HTML的话:
切图输出其实也已经算一种自动化。然而现在还能使用jade、HAML或者各种模板引擎生成,也就是有可能不是直接手写HTML代码了。这个就需要依赖编译了。
CSS的话:
SASS、LESS和Stylus都已经存在了很久了,源代码产出CSS也是需要编译的。CSS文件也能够进行合并和版本控制,如此一来又需要额外的工具。
Javascript的话:
本身就是一个编程语言,有工具能对其语法进行排错,不能不用吧?流行又高效的模块化开发,需要工具合并吧?压缩源代码,又需要操作了吧?注释呢?文档呢?统统需要工具啊。
总结起来,HTML要编译,CSS要编译、合并、压缩和,Javascript要编译/合并、压缩甚至生成文档。最后发布还要顾及CDN或者缓存或者bug跟踪进行版本管理如果以上每一步都要自己操作,那么即使只是打命令行也是够呛。
而使用上自动化构建,则在设定好以上多种工具的使用流程之后(几乎)一劳永逸,只需要专心写好流程最开始的源代码就OK,构建工具会完全自动地生成最终结果。能少干活就少干活,那个程序员愿意做重复性工作?
这也就是为什么自动化构建工具在一日发展千里、需求一日多改的前端如此受欢迎的原因了。
是个程序员总会遇到圣战的时候,或是Emacas VS Vim,或是C# VS Java,或是Python VS Ruby,或是AngularJS VS ReactJS,或是IOS VS Android……
当然,PHP是最好的语言所以不用战争。
也有人只是选择困难症后期患者,一旦选项多于一就会头痛欲裂、浑身不自在。
那么,究竟Grunt or Gulp?
为此很多人写过分析的文章,有中文的、英文的和另一篇英文的,总的来说就是,
Grunt:插件比较多,社区成熟,风格偏配置,插件比较混乱,代码较长,过程有临时目录
Gulp:插件不够Grunt多,风格偏代码,插件功能单一专注,代码较短,流式工作无需临时目录
个人选择是Gulp,那个插件数量不够多是个伪缺点,只是不过Grunt多,其实也有上千个,还不够用?!从其他优点来看都是完胜Grunt了。
那是不是选择Gulp来构建i18n.js呢?
并不是。
如果有仔细看给出的分析文章,可以看到还有一个构建工具:npm。
众所周知npm实际上是nodejs的包管理工具,然而在其配置文件package.json里面却也可以设置一些可运行项,然后通过npm run xxx
来运行。从文章来看,也是能够胜任构建的任务。
那么问题来了,从网上基本千篇一律的教程来看,Grunt和Gulp的使用都是装上了自带npm的node,然后通过npm来安装的。既然npm本身就能作为构建工具,那为啥要用Grunt和Gulp?
注意到那篇中文的分析文章还提到“npm一般用在个人项目里,对于团队项目则不适用”,然而果真如此吗?
使用英文搜索一下,不难发现国外也有人提出停止使用Grunt和Gulp的主张,在文中列出类似或同类构建工具的问题:
接着提出了使用npm的主张,并且还给出了详细方法,可以看到使用npm更易懂更简洁。
我使用Grunt和Gulp的经验并不多(实际也不是什么复杂的东西),对于文中提出的第一个问题已经深有感触。明明只是简单的工作,却要写一大堆罗嗦的配置。另外Grunt/Gulp插件使用都是local安装,于是明明只是写几个KB大小的库,却要将项目的文件夹弄成几十MB大。插件作用都很专一,更新频率很低,全局安装就好,每开一个项目就独立往项目塞一样的工具简直是闲得蛋疼,尤其npm下载插件经常由于网络原因而失败。
当然独立安装项目依赖也有其存在的意义。当将项目发布给其他人使用或者开发的时候,独立安装项目依赖可以保证环境是一样的。
所以结论是,不要为使用Grunt/Gulp而使用Grunt/Gulp,很多情况下并不需要将事情弄复杂。
参考国外配置npm的文章,写好package.json。
1 | { |
清晰明了。
测试环境清理:rimraf
HTML构建:jade
Javascript排错:jshint
Javascript合并:concat-cli(多个文件复制合并)
全部都是一句话配置,直指命令行。多个任务最终又可以汇集在test
/test:watch
中。
使用concat-cli构建Javascript比较少见,更多的是使用browserify配合require语法。然而i18n.js库实在太小了,真的不需要复杂的模块化管理。
先将原js文件拆分成三个。
1 | // prefix.js |
1 | // suffix.js |
1 | // main.js |
接下来可以专心在’main.js’中写代码了。
在敲入代码之前记得使用
npm run watch:js
,不然配置毫无意义。
1 | // Save the global object, which is window in browser / global in Node.js. |
TRANSLATION_TABLE
保存翻译文本,CURRENT_LANGUAGE
保存当前使用的语言,_
是内部使用的命名空间。另外使用root
保存全局对象,previousi18n
保存之前已存在的’i18n’对象。
1 | // Restore the previous value of 'i18n' and return our own i18n object. |
noConflict函数,学jQuery的。
1 | // Load the translation table |
载入翻译文本,使用深复制(应对多层对象)。
1 | // Return the current set language |
返回当前使用的语言。
1 | // Change the language, apply to all cached nodes or document.body |
切换语言。流程是匹配出语言配置,再从body开始抓取出需要翻译的DOM元素(_.filterNodes函数),然后翻译(_.translate函数),最后设置当前语言。
API函数的内容写得简单,主要是需要基于不少的内部函数。
首先是深复制。
Javascript中的赋值都是复制,因此对于基本类型(primitive value):Undefined、Null、Boolean、Number、String来说,直接赋值就是复制。其他的复杂类型,直接赋值同样是复制——然而,复制的是引用,并不是引用的对象。
1 | // can handle array and nested objects, not perfect |
改写自jQuery1.7内部实现的对象深复制函数,只保留了识别数组和对象的功能。因为译文文本就是JSON格式的普通对象(plain object),无需要实现太复杂的复制。核心代码的思想就是检测在当前对象的每一个属性(省略了hasOwnProperty的检测),如果是数组(_.isArray)或者普通对象(_.isObject),则实实在在创建一个数组 / 对象以供复制。
而数组 / 对象检测则是用以下代码:
1 | // figure out array |
而库的核心,一个带访问函数的DFS。DOM操作自带取子元素和兄弟元素,写起来很简单。
1 | // Walk the DOM, call the visit |
通过查看元素的属性来筛选出将要翻译的元素。
1 | // Returns array of elements that have attribute 'data-i18n' |
上一个函数中用到的’_.hasAttr’,特别实现是因为IE的取属性方式跟其他浏览器不一样。
1 | // Return true if ele has attribute otherwise false |
接下来是改变元素的文本。代码很简单,做的事情就是遍历DOM元素数组,取属性’data-i18n’的值作为key值,在译文表格中查询value值(_.getTranslatedText),最后改变元素的文本(_.setText)。
1 | // Translate each node in array with given language table |
_.getTranslatedText 支持使用点记法,代码直接用以前写过的。参考
1 | // get translation via path, support dot |
_.setText 函数就是用’innerText’或’textContent’来设置元素文本。
1 | // cross-browser set text |
看起来大概写完了,来写一些测试。
实际上应该先写测试,再写代码。但是一来库很小,二来我不太懂,所以……不过之后写比较大型的库的时候要好好地用mocha等的测试框架。
用jade语法写一个HTML文件。
1 | doctype html |
控制台运行npm run test:html
生成HTML文件,用浏览器打开,切换一下语言,没问题。
应用i18n.js的多语言页面,是有可能动态添加DOM元素的(AJAX拉取数据之类的操作),所以i18n.js库也需要将添加的DOM元素翻译一下。于是再添加一个名为’translate’的API好了。
由于需要同时修改jade文件和js文件,所以使用
npm run test:watch
,同时监视jade文件和js文件的变化。
1 | // Translate nodes |
做的事其实和use
大同小异,只是目标DOM元素不一样。
修改一下测试文件,增加一点代码。
在 body 中添加两个按钮。
1 | button(data-i18n="BUTTON_ADD_1",onclick="add(1)") add one |
在数据中增加按钮的文本。
1 | 'BUTTON_ADD_1': 'add one line', |
在脚本中增加一个函数,用作模拟动态添加DOM元素。可以添加一个或多个DOM元素。
1 | function add ( num ) { |
再使用浏览器测试一下,同样没问题了。
现在main.js
文件看起来比较复杂,可以再分别拆分成var.js
,存放顶层变量;util.js
,包含内部的函数;api.js
,包含库的API。
稍微修改一下package.json
文件,相关位置改成拆分后的文件。
1 | "lint": "jshint src/js/var.js && jshint src/js/util.js && jshint src/js/api.js", |
最后运行npm run build
将js库编译出来并压缩。
原来的脚本的功能就不再详述了,来说说一下新版脚本三大功能,分别是:探索广播、下载广播和查看广播。
探索广播是指能够列出某一个广播站上所有的 / 当天的 / 星期 x 的 / 最新的广播,旨在能够帮助使用者发现自己喜欢的广播和新推出的广播。
下载广播是指能提取出广播音频 / 图片的地址,供第三方播放器播放或保存。
查看广播是指能够列出某广播的主要信息:包括更新日期、最新一期的内容、主持人等。
基本可以说,有了新版的脚本,基本就不需要用浏览器浏览广播站的网页了。并且,因为脚本只是需要抓单个页面和将网页内容整理好再输出,所以对比起网页,能更快更高效地呈现有用的信息。
对比旧版,新版脚本的一大改进是引入了子命令。最直观的反映就是,调用方式的不同。
旧版1
python www.py -x -y zzz
新版1
python www.py xxx -y zzz
就是类似 git
命令行的那样,argparse
库非常给力地支持这种方法。
重点是使用 add_subparsers
函数和 add_parser
函数,详细使用看 文档 和github 上的代码。
多亏了能够这样嵌套命令,即使再多的功能也能变得清晰分明。
ver 2.0 的起点是一个脚本框架,承载参数的解释和自定义函数调用的重要功能。
脚本的一次执行基本流程是:
入口 → 解析参数 → 调用自定义函数 → 执行自定义函数
前三个都能够定下来,不同的广播站只需要各自填充自定义函数就可以了。
最简框架代码如下:
1 | """自定义函数""" |
框架还基于库 urllib
、urllib2
、argparse
和 bs4
,制定了自定义函数的骨架。
脚本做的事情,无非就是获取网页资源,接着在 html 结构中筛选有用信息,然后格式化成文本输出。因此,自定义函数也可以制定出骨架,不同的广播站就只需要指定筛选的规则就行了。
1 | # 获取网页 |
虽说骨架是定下来了,但在特殊函数比如下载函数就需要另外编写。
虽然新版脚本框架是为了 radioit 计划而写的代码,但是其中 argparse
模块的使用的代码也是能被借鉴在其他需要带参数运行的 python 脚本中。
新版对比旧版,增加了功能,但是也增加了使用的复杂度和代码的长度。旧版还是可以保留下来,供日常快速使用。
]]>