参考《社会心理学》 David G. Myers 第8版

社会心理学 Part 1 - 导论 & 社会思维 社会心理学 Part 2 - 社会影响 社会心理学 Part 4 - 应用

这部分主要讨论社会中个体是如何联系的。

偏见

人们对于肥胖者就有明显的偏见:缺少魅力、不太聪明、不够成功、缺少修养。

界定偏见

  • 偏见即对一个群体及其个体成员预先的负面判断
  • 它是一种态度
  • 负面评价是偏见的标志
  • 它源于行为辩解的需要,或是负面的刻板印象

刻板印象是对人群先入为主的推测,比如南方人感性,北方人粗犷。本身反映了文化或生理对人群的一种期望,并无大碍。但刻板印象过度概括或明显错误时,就形成了偏见。偏见的行为表示称为歧视。歧视包括种族歧视、性别歧视等等。

种族偏见

关于种族偏见有下面一些研究结果:

  • 多数人只能看到其他人身上的偏见
  • 人们在最亲密的社交领域会表现出最明显的偏见,如婚姻、看病。
  • 种族偏见随着年代发展,文化多样性深入,逐渐由明面表示变成较难察觉的内隐态度。这些态度以一种潜意识的体现。
  • 偏见有一些微妙的隐式表示,如过度的种族敏感性、对成功赞扬过度,对过失批评过度等

性别偏见

性别的刻板印象不仅持有印象的人有,刻板化群体成员也接受这种刻板印象。比如,视男性为领导者的刻板印象。刻板印象并不是偏见,只是为有的偏见提供支持。除了坏的刻板印象外,也有好的刻板印象。比如,大部分人更喜欢女性而非男性, 即女性妙效应

类似种族偏见,性别偏见已由堂而皇之转为微妙的偏见。

社会情境

偏见的来源复杂,社会来源是一部分。

  • 不平等的社会地位滋生偏见。偏见可以帮助有钱有势的人将其经济和社会特权合理化。我们敬重地位高的人具有的能力,同时也喜爱那些能欣然接受自己较低地位的人
  • 社会支配性取向会影响偏见的接受程度。社会支配性高(即控制欲强)的人乐于接受偏见,比如反对破坏阶级等级的政策
  • 权威人格的人,在孩提时期往往经历过苛刻的规矩。这可能会导致他们压抑自己的敌意和冲动,并将之投射到外群体身上
  • 调查发现,宗教和偏见的相关并不能得出因果关系
  • 偏见在被社会接受后,会有许多人随着从众走上相同的道路
  • 有些社会制度也在不知不觉支持着偏见。比如,杂志和报纸上的男性照片多数专注于面部,而女性则不到1/3,因为面孔突出的人被认为更有智慧、抱负和主见。电影、电视上的形象能会强化盛行的文化态度。

动机根源

挫折与攻击:替罪羊理论

我们遭遇挫折的原因令人胆怯或莫名其妙时,往往会转移敌对方向。因此,往往繁荣时期更容易维护民族和睦。现实群体冲突理论认为有同样需求的物种竞争将最大化。如,敌视黑人的偏见在经济地位和黑人最接近的拜仁身上最为强烈。

社会同一性理论:感觉比他人优越

我们在社会中的自我感觉不仅仅包含自己,还包含自己的社会群体。我们将自己和群体联系在一起获得自尊,将自己群体和其他群体比较,并偏爱自己的群体。即社会同一性

人们具有内群性偏见,即群体内比群体外好。这也是人们寻求积极自我概念的一个表现形式。当我们群体规模较小、经济地位较低时,我们会更容易表现出内群性偏见。即便是毫无逻辑依据的群体意识,也能产生内群性偏见,比如掷硬币的正反。当我们的群体表现较好时,我们会更强烈地认同该群体,从而使自己感觉更好;相反在表现不好时,内群性偏见就不那么明显。

偏见本身就可以让人获得高人一等的感觉,这本身就是一种心理学收益。对比自尊没有受到威胁的被试,体验到挫折感的学生对自己学校的评价更高;被引发出不安感的被试,在评价他人工作时更加苛刻。甚至,思考自己的死亡问题也会引发人们足够的不安全感以强化内群体偏好外群体偏见。自尊受到威胁时,人们会诋毁外群体,已恢复自尊和社会同一性。相反,一旦归属感得到满足,人们就会更为接纳外群体。

综上,那么怎么避免偏见的动机呢?

首先,深植于潜意识中的偏见没那么好压抑。但也并非完全无法避免,比如产生内疚感,从内在角度出发避免偏见。

认知根源

知觉错觉是我们解释世界技巧的副产品。刻板印象是我们简化复杂世界的副产品。

类别化

我们简化环境的方法之一就是分类。刻板印象也是类别化的一种体现。实际场景中还有些因素让我们依赖刻板印象:

  • 时间紧迫
  • 心事重重
  • 疲惫不堪
  • 情绪激昂
  • 年轻气盛而无法欣赏多样性

实际上,我们会借助一些典型特征对眼前的人自发类别化,如中年商人,知识分子等。偏见建立在分类的基础上。

在我们把人划分成群体时,有可能会夸大群体内部的一致性和群体间的差异性,即外群体同质效应。,比如从群体决策中高估一个群体的全体一致性。我们越是熟悉一个社会群体,就越能看到起内部的多样性。比如,与我们自己种族的人相比,其中种族的人看起来更为相像

独特性

具有独特性的人,身上的优点和缺点都会被夸大。吸引我们注意力的人,看似对所发生的一些具有更大的责任。除了注意到独特的人,人们也会关注的那些违背期望的人。

作为独特的人本身,我们有时会错误认为,他人对我们的反应是针对我们的独特性而来,从而曲解他人的行为方式。因此即使双方都是善意的,一个强势的人和一个弱势的人之间自我意识的相互作用也会令双方紧张。同时,人们的污名意识会提醒自己在多大程度预期他人对他们产生刻板印象。

独特的案例也会加深刻板印象。人们倾向于从个别生动案例来作出概括。独特、极端的案例具有吸引注意力的的效果。因此我们的对一个群体了解越少,就越容易受到少数生动案例的影响。刻板印象假定群体成员和个人特征间存在某种相关性,这其中自然存在虚假相关。

归因

人们的归因错误也影响着偏见的形成。比如我们总是热衷于将人们的行为归因于他们的内在倾向,而较为忽略重要的情境力量。

利群偏差

我们倾向于关注内群体的积极行为和忽视外群体的。内群体成员的积极行为往往被描述成一种普遍品质,而外群体成员则常被描述成一个特定的、孤立的行为。这在强调谦虚的群体或地位较低的群体中会不那么明显。

责备受害者可以起到为指责者本人优越地位辩护的作用。责备的出现是因为人们把外群体的失败归结于群体成员的内在品性有问题。

公正世界现象

当观察者无力改变受害者命运时,他们就会否定和贬低受害者。公正世界现象即指人们认为“我是一个公正的人,生活在一个公正的世界,因此这个世界的人们得到的是他们应得的东西”(可怜之人必有可恨之处)。这样的认识使人们有贬低受害者的倾向。

偏见的后果

长期保留的刻板印象

我们的预断会引导我们的注意、判断和记忆。在刻板印象形成后,如果群体成员行为验证了期望,则会加深印象;相反,我们会对特殊情况闪烁其词。在现实中的体现就是,你无论怎么努力都无法摆脱某人对你的评价,一旦某人预期和你见面不愉快时,误会就很可能发生。

同时,当群体中出现特殊个体不符合刻板印象时,我们会通过分出一个子类的方式来保证自身认识的自洽,即再分类法再分群法。子类作为群体的一部分获得承认。

自我实现的预言

社会信念会自我验证,偏见会对其对象产生影响,向自我实现的方向发展。

刻板形象威胁

他人的刻板印象会威胁到个体表现,从而进一步印证刻板印象。比如,在暗示女性“没头脑”的刻板印象后,其在数学等理科表现上会变差。相反,正面的刻板印象可以促进成绩。

个体判断的偏差

幸运的是,这种偏差有,但是人们在评价个体时,往往比评价个体构成的群体更为积极。因为往往群体成员琐碎但生动的信息效果上好于群体泛泛的信息。

不过强烈而且显然相关的刻板印象还是会影响我们对个体的判断。同时,刻板印象会影响我们对事件的解释,这在我们对某人的信息模棱两可时较为明显。

在人们的行为违背了我们的刻板印象时,人们会做出比较极端的评价。

攻击行为

攻击行为即意图伤害他人的身体行为或言语行为。攻击行为按目的不同还可以分为两类:敌意性攻击行为由愤怒引起,以伤害为目的;工具性攻击行为只是把伤害作为达到其他目的的一种手段。

理论基础

生物学

本能论认为人类的攻击行为源自自我破坏的原始冲动,这种冲动运用到他人身上时,成为了攻击行为。进化心理学认为攻击行为对于获取资源、抵抗攻击、威吓甚至干掉情敌、防止配偶不忠都是一种有效的策略,可以帮助他们的基因得到更高的保留几率。

遗传基因神经系统都对攻击行为有影响作用。另外,生物化学因素也会有影响,如激素和酒精。睾丸激素可以刺激人的攻击行为,神经递质5-羟色胺可以控制冲动,缺乏5-羟色胺的人群自我约束力较低,更愿意承担风险。

挫折-攻击理论

最早对攻击行为的心理学解释理论是挫折——攻击理论,即挫折总会导致某种形式的攻击行为。而攻击能量并非总是朝向挫折源,当对方会表示反对或惩罚时,我们会将敌意转移到安全的目标上。

而后人们发现,并不是所有挫折都会指向攻击行为,也有一些其他因素会导致攻击行为。伯科威茨认为原有理论夸大了挫折和攻击行为间的关联,他认为挫折产生的只是愤怒——攻击行为的一种情绪预备状态,一旦有共计线索出现就会诱使人们产生攻击行为。愤怒起源于某个有其他行为选择的人阻挠了我们实现目标。

“当不幸看上去不可避免时,人们可以耐心地承受;一旦人们感到他们可以摆脱这些不幸,它们就变得令人无法忍受了。 —— 托克维尔 1856

挫折剥夺是两个要区分开的概念。挫折是指一个人得不到有吸引力的或是值得追求的目标;挫折是指他未能从这个对象上获得原本预期可以得到的快乐。这也是为什么生活水平改善并不会消除攻击行为,反而有所促进。当预期和现实有差距时,挫折感便产生了,即期望和实际所得间的差距产生挫折感相对挫折是指我们把自己和他人比较时带来的挫折感。因此,电视、广告甚至朋友圈中描绘的幸福生活也是挫折感的一个可能来源。

社会学习理论

这种理论认为学习同样可以引导攻击行为。

人们可以习得攻击行为的回报,此时,攻击行为变成了为了得到特定回报而采取的手段。比如,恐怖主义可以让无职无权的极端分子获得全世界的关注。通过观察别人,人们也可以进行同样的学习,尤其是未成年的孩子。观察攻击行为不仅降低了他们的自我控制,还教会了他们怎样去攻击。身体富有攻击性的儿童往往来自惯用体罚的家长;得不到父亲关怀的孩子,暴力犯罪的可能性更高。

文化对攻击行为也有所影响,来自崇尚荣誉的文化的人,更易拥有攻击性的心理倾向。

影响因素

攻击行为的诱发因素有厌恶事件唤醒媒体群体氛围

厌恶事件

厌恶事件即引起厌恶感受的体验:

  • 疼痛
  • 不适的炎热
  • 受攻击
  • 过度拥挤

在动物身上的实验表明,遭受的待遇越残酷,它们对同伴施加的行为就越残忍。电击、炎热、“心理疼痛”(受挫)都可以引发攻击。厌恶的气味、香烟味、空气污染和攻击行为都有联系,但是环境因素中炎热是研究较多的。炎热高温会提升报复行为的几率和唤起我们带有敌意的想法。另外,收到攻击或侮辱很容易引起攻击行为。

唤醒

性唤醒和愤怒等其他形式的唤醒形式是可以相互增强的。挫败、酷热或侮辱性的情境都会提高我们的唤醒水平,这种情况一旦发生,唤醒状态就会和敌对想法、情绪一起构成攻击行为

攻击线索

前文说过,在攻击线索的触发下,攻击行为最容易发生。枪支就会启动敌对性想法和惩罚性判断。它不仅让暴力成为可能,也可以刺激它的发生。枪支不只是提供线索,同时也能拉大攻击者和受害者的距离,让我们更为残忍。

媒体影响:色情文学与性暴力

色情小说中的色情情节会:1)歪曲女人对性攻击的真实态度;2)增加男人对女人的攻击行为。在实验室实验中,连续观看3周性暴力电影后,男性被试对性暴力的焦虑水平显著下降,表现出对家庭暴力受害者更少的同情,且对受害者受伤程度估计偏低。

色情小说也会导致男性对女性实际的攻击行为。反复观看以强迫性行为为特征的色情影片容易导致:

  • 性伴侣吸引力下降
  • 对通奸和女性对男性的性顺从更容易接受
  • 对女性感知更容易从性的角度出发

因此,在媒体意识教育中,需要能够让受众重新认识女性对性暴力的真实态度

媒体影响:电视 & 游戏

20世纪末,人们在电视上花的时间越来越多。宣泄假说认为观看暴力节目可以帮助人们释放被压抑的敌意,实际是这样么?

在看电视和行为的相关研究和实验研究中表明,电视和暴力不仅有相关关系,也有因果关系,即观看暴力会导致攻击增加。尤其是,当一个有魅力的人因正当原因实施适当暴力,这种暴力未受惩罚也未表现出伤害时,观看暴力节目的效果最为显著。暴力节目在三方面增加攻击行为:

  • 暴力节目会造成唤醒状态
  • 观看暴力会使人们降低抑制
  • 媒体内容会引起模仿

电视上的亲社会行为虽然也可以教孩子学习积极行为,但是节目中的攻击行为远远超出爱抚行为。电视在影响行为的同时,也影响着人的思想:

  • 重复观看暴力节目会产生脱敏作用,即对暴力的情绪敏感度下降。而暴力、尖叫、裸露镜头在电视节目中越来越常见
  • 类似节目会改变知觉。看电视多的人会更容易夸张周围世界暴力发生的频率,更害怕人身攻击,更容易产生脆弱感
  • 观看暴力节目会启动潜意识中的相关认知,如对攻击性词汇更敏感

类似地,电子游戏中的暴力血腥内容也在增多,且相比电视节目,参与感更强, 且会从攻击中获得奖赏,更易诱导人们做出攻击性行为和更富侵略性。和非暴力游戏比,玩暴力游戏时:

  • 唤醒水平提高
  • 引发攻击性思维
  • 唤醒攻击性情绪
  • 诱发攻击性行为
  • 减少亲社会行为

过度地模拟暴力行为会促使攻击性倾向增强,而非宣泄暴力情绪。

群体影响

正如群体极化中提到的,群体通过责任扩散使攻击行为增大。服从的压力和去个体化使群体成员的自我认同在自身完全投入群体后逐渐消失。群体思维动员一个群体或文化做出异乎寻常的举动是常见现象。如对卢旺达图西族人的大屠杀。群体可以强化攻击倾向。

如何减少攻击

分析了攻击的影响因素后,如何减少攻击行为呢?

宣泄即释放聚集的攻击能量。在亚里士多德时代就有宣泄的说法,不过社会心理学家一致认为,与弗洛伊德等人的猜想相反,通过暴力行为不能实现宣泄,反而会增加攻击性。同样的,表达敌意会导致更多敌意

如果攻击行为是习得的,那么减少厌恶体验可以减少攻击行为。

  • 避免给人们错误的、不可达到的预期,适当奖励合作性的非攻击性行为
  • 如果要惩罚,需要的是惩罚的必然性而非严厉性。提高逮捕率而非刑期会更显著地带来犯罪量减少
  • 体罚会产生消极作用,根据以前的观点,很强的外部原因并不能内化孩子的行为。可以在孩子很小时奖励敏感性和合作,用非暴力的方式教育孩子达到此目的,积极地表达观点(“清理完你的房间,你就可以出去玩了”而不是“如果不清理好房间,你就别想出去玩”)
  • 减少电视和电影上野蛮、色情、缺乏人性的表演
  • 增加武器(如手枪)的获取难度,避免攻击性刺激

有一种观点认为,当类似问题经常出现时,要追根溯源思考根本原因而非表面原因——改造我们的文化,挑战那些腐蚀年轻人的社会毒瘤和重建我们的道德根基。

亲密关系:喜欢和爱

人和人之间终生的相互依赖性使人际关系成为我们生存的关键。我们会有一种强烈的归属需要,即与他人建立持续且亲密关系的需要。

  • 从进化角度看,相互依存才能使族群得以繁衍生息
  • 正是因为人们渴望爱和被爱,才会在化妆品、服装和塑形上有巨额投入
  • 社会关系的损伤会影响情绪。人们被拒绝时,会感到抑郁和生活乏味。相反,当我们感到被亲密关系支持时,会更加健康和快乐。
  • 死亡会提醒我们更重视归属需要,重视与他人的关系并与我们所爱的人保持亲密
  • 即使在虚拟世界里,被一个永远不可能见面的人拒绝,也会引起挫败感。这种创伤感会在大脑内有真实的生理对应

可见,内心深处的归属需要得不到满足时,就会使我们感到不安。

造就友谊和吸引的因素

接近性外表吸引力相似性被喜欢的感觉

接近性

接近性相对于产生敌意,更容易产生喜欢。对比地理距离功能性距离——人们生活轨迹相交的频率才是关键。有研究者猜测,只要是经常在一起,我们会爱上几乎是任何一个与自己有着大致相同的人格特征并且会回报我们感情的人。

接近诱发喜欢的其中一个原因就是易得性——更易产生相互交往。近距离会使人感到亲近。更甚者,仅仅是对相互交往的预期就可以引发喜欢。对一个人约会的预期也能促进喜欢。

曝光效应

各种实验表明,熟悉并不会引发轻视。实际上,熟悉诱发了喜欢。各种新异刺激——无意义音节、汉字、音乐片段、面孔——的曝光都能提高人们对它们的评价。

  • 不同语言和年龄的人都偏好自己名字中的字母或母语中频繁出现的字母
  • 人们对熟悉的面孔评价更高,人们更喜欢镜子中镜像版的自己
  • 人们喜欢和自己相关的事物,如倾向于居住和自己名字相关的城市或选择和名字相关的职业
  • 广告商和政治家们利用这个效应,即使人们对某商品或候选人没什么倾向性,仅仅通过简单的重复(电梯广告的病毒式营销),也可以增加商品销量或得票率。
  • 在广告或政治标语中,通常使用简短的slogan代替唱片大陆,突出候选人名字或商品关键价值

Zajonc通过实验发现,人们的直接感受并不借助于意识存在。他认为,情绪相比于思维是更即时的东西,情绪半独立于思维,可以先于认知。曝光效应会引发愉快的情绪,但是当重复没完没了时也会引起兴趣减弱。

1990华盛顿州的一次法官选举中,在没有开展竞选活动的情况下,名不见经传的候选人Charles Johnson因为姓名更熟悉战胜了Keith Carlo。

外表吸引力

人们都说外貌不重要,但行为上却不总是这样。现在的很多研究表示:外貌的确是很重要的。从某个角度说,美貌的确是一种财富。

约会相关

年轻女士的外表吸引力可以中度预测她约会的次数,而一位男性的外表对他约会的次数相关性则要小一些。哲学家罗素认为:“整体上来说,女人倾向于因性格而爱上男人,男人则倾向于因外表而爱上女人”。哈特菲尔德在明尼苏达大学的实验证实了这一点:男人的确更在意异性的外表吸引力。某位女性外表吸引力越大,男性就越喜欢他;当然,男性的外表吸引力也有童颜的效果。美貌使人愉悦。

匹配现象

人们一般与和自己同等吸引力的人结为伴侣。人们在选择伴侣上,尤其是终身伴侣上,倾向于选择不仅在智力,而且在外表吸引力方面和自己匹配的人。

外表上的匹配有利于良好关系的发展和维持。但是很多夫妻的吸引力并不匹配,却仍然幸福。这种情况下,吸引力较差的一方常常有其他方面的品质予以补偿。男性通常强调自己的财富和地位,并且希望寻求年轻和有吸引力法女性;女性则相反

刻板印象

在儿童中,老师们倾向于认为那些有吸引力的孩子在学习上更聪明。所谓的巴特·辛普森效应即大多数人都认为,长相一般的孩子,他们的才干和社交技能都不如那些漂亮的同龄人。同时,在其他方面都几乎相同的情况下,我们仍会猜测漂亮的人更快乐、性感热情、更开朗、聪明和成功。这就是外表吸引力的刻板印象——美的就是好的。

小时候的童话里,白雪公主是善良且美丽的,而巫婆则是邪恶和丑陋的。尽管两者间并不会有明显相关性。

也是因为这样的刻板印象存在,20世纪70年代后,妇女在化妆品和整容方面的投资大大增加,对自己外貌深感不满的人也越来越多。Michael Kalick在1977年让学生们观看8位女士整形手术前后的照片,结果表明,被试不仅认为女女士们在术后更具吸引力,也认为她们更善良、敏锐、性感热情、更具责任感。

在城市化进程加快的背景下,人和人接触的时间越来越短暂,能给人留下第一印象的外表吸引力就显得愈发重要。在面试中,吸引力和外表修饰影响着第一印象甚至薪水。

之前我们提高过的自我实现的预言在这里也发挥着作用。研究表明,有吸引力的孩子和青年,某种程度他们的确不那么拘谨,更加外向且社交技能更强。从而在正反馈下变得更受欢迎。

长得漂亮没有坏处么?哈特菲尔德等人认为漂亮也会带来不快的性骚扰、同性的嫉妒和排斥,错误归因他人的欣赏。

如何衡量吸引力

讽刺的是,真正的吸引力是完美的平均——由计算机平均出来的平均面孔就很有吸引力。它们也会带来一种熟悉感。

从生物学角度讲,健康、年轻等富有生殖能力的女性特征以及能提供资源、保护能力的男性特征被认为具有吸引力。男性希望女性要有适度的外表吸引力,而女性则希望男性拥有财富和地位,且两性都喜欢有爱心的人和聪明的人。这是进化和文化共同造就的。从深层角度讲,我们的潜意识也被本能影响着。

社会比较会影响吸引力评估。

  • 刚看过描述有吸引力异性作品的被试对比对照组对异性的评价更低
  • 性唤起暂时性使异性看起来更具吸引力
  • 对比效应在女性上更为明显
  • 情人眼里出西施。我们会认为我们喜欢的人以及和我们相似的人具有吸引力。人们爱得越强烈,就越不觉得任何其他异性吸引人

相似 or 互补

找一个性格相似的人还是性格互补的人做伴侣?实验显示,

  • 丈夫和妻子间相似性越大,他们就越幸福且不容易离婚
  • 某人态度的相似性会引发我们的喜欢。
  • 人们喜欢和他们想法一致和言行一致的人。模仿可以促进和谐。
  • 人们渴望相似伴侣的愿望要远远强于渴望漂亮伴侣的愿望

相反,不同态度对喜欢的抑制甚于相似态度对喜欢的促进。态度一致性有助于人们促进和维持亲密关系。在恋人这样的亲密关系外,思想上的相似产生的吸引力比肤色的相似性更重要。不过这个世界是具有文化和思想多样性的,我们也需要尊重和欣赏这种差异,求同存异。

人格特质上的互补可以引发吸引么?研究者考察了这个问题,发现在各个方面,相似性仍然是主导因素。互补性产生的对比效果可能使一方感受更糟糕,如一个悲观的人和乐天的人在一起。另一方面,我们通常也不会认为那些表现出和我们相反的不好的特征的人是具有吸引力的。

喜欢我们的人

喜欢通常是相互的(Really?)。接近性和吸引力可以影响我们最初被谁吸引,相似性会影响长期的吸引。一个人喜欢他人的程度可以反过来预测对方喜欢他的程度(Really?)告知某些人他们被被人喜欢或仰慕时,他们就会产生一种回报的情感。因为通常来说,缺点比优点更具影响力,在我们评价别人和别人反过来评价我们时,消极消息占比都更多

  • 坏心情比好心情更能影响我们的思维和记忆
  • 坏事比一件好事更能产生更持久的效应
  • 坏名声比好名声更易获得,且更难摆脱
  • 糟糕的健康状态产生的痛苦远大于舒服产生的快乐

奉承通常会使人感觉良好,但我们会认为批评比表扬更真诚。在赞美违背了事实后,我们很容易归因到别有用心的动机上。在一项实验中,被试大学生们被要求给出和女/男朋友在一起的原因,那些注意力被引到外在原因的人,相对于引到内在原因的人,会表现出对恋人更少的爱恋和结婚可能性。

人们在自尊受到创伤的拒绝后,会表现出反弹行为——对伴侣评价更高,陷入更加激情的恋爱。相反,在获得尊重时,实验发现个体逐渐获得尊重,且推翻了目标任务原先的批评时,个体会更喜欢这个目标人物。批评之后的赞美之词才更为可信。频繁的赞扬可能会失去价值。因此,对比“过度赞扬”,保持坦率而真诚的关系——相互尊重、彼此接纳、保持忠诚——更可以让对方感到满意。真诚对营造两人间的良好关系很重要。

关系中的回报

吸引涉及两方——被吸引的一方和吸引他人的一方。“我喜欢XXX,是因为和她在一起感觉如何如何”。可以将这个观点总结为简单的吸引的回报理论我们喜欢那些回报我们或与我们得到回报相关的人。推广一点,我们还喜欢和那些能让我们心情愉悦的人交往。这种“联系——喜欢”在多个实验中都被证实。

  • 舒适的环境能激发被试对被评价者的好感
  • 如果你希望维系和伴侣的关系,那么继续把你们的关系和美好事物联系起来很重要
  • 接近性带来报偿,因为这种友谊付出的时间和精力都较小
  • 我们喜欢有吸引力的人,因为他们身上可贵的品质能让我们受益
  • 他人观点和我们相似,也会让我们觉得得到了回报。我们尤其喜欢那些被我们说服的,并开始认同我们观点的人
  • 我们喜欢被人喜欢和被人所爱

爱情

爱情比喜欢更复杂。对爱情的研究首先也要建立界定和衡量的手段。Sternberg认为爱情由激情、亲密和忠诚组成:

  • 激情 + 亲密 = 浪漫之爱
  • 激情 + 承诺 = 游戏之爱
  • 亲密 + 承诺 = 友谊之爱

激情之爱即热恋状态——强烈渴望和对方在一起的一种状态。研究表明,持续的目光接触、点头和微笑都是激情之爱的标志。哈特菲尔德认为:任何一种生理唤醒状态最终都可以被归因为某种情绪。激情之爱就是我们在生理上被有吸引力的人唤醒所感知到的心理体验。这么来看,任何一种可以增加兴奋感的东西都应该可以增强对爱情的感受。沙科特和辛格提出的情绪两因素理论认为,当处于兴奋状态的男性对女性做出反应时,他们很容易把自己的某些生理唤醒错误地归因于这位女性。一些研究表明,生理唤醒可以促进罗曼蒂克式的反应。

  • 观看恐怖电影、乘坐过山车、体育锻炼、饮酒、畅谈等促进生理唤醒的活动都有同样效果,特别是对那些我们觉得有吸引力的人
  • 夫妻双方共同完成一项可以提高激活水平的活动后,会对其关系的总体情况报告更高的满意度

和通常的观念相反,许多研究发现,其实男人比女人更容易坠入情网。相比于女性,男性很少技术一段即将迈入婚姻的爱情关系。但是,热恋中的女性也会有和伴侣一样多的感情投入,甚至更多。女性似乎比男性更注重友谊中的亲密感,也会更多关心她们的伴侣。而男性则更多地想到恋爱中的嬉戏以及性的方面。

伴侣之爱

尽管激情之爱可以热火朝天,但终归还是会平静下来。如果一段亲密的感情经受得住时间的考验,那么它最终就会成为稳固而温馨的爱情,哈特菲尔德称为伴侣之爱(companionate love)。伴侣之爱相对平和,是一种深沉的情感依恋。

和吸毒的戒断效应以及抗药性很像,浪漫爱情会有产生之初的高峰和逐渐消退的趋势。曾经很大刺激的用量现在变得效果轻微了,然而一旦停止用药,并不能使你恢复原先状态,而是会引发强烈的戒断反应。那些分手、离异的人会吃惊的发现,虽然早已对另一半失去强烈爱恋,但分开后,生活居然如此空虚。

自由恋爱的夫妻在5年以上,会觉得彼此的“有爱情”的感觉越来越少,相反,那些包办婚姻的夫妇则报告逐渐增长的爱情体验。当然整体上,自由选择伴侣的女性更多地感到快乐。在过去20年陡然增高的离婚率,至少部分源自人们越来越多强调强烈积极的情绪体验在生活中的重要性。这一点在偏向集体主义的文化则并中不明显。热恋中的相互迷恋逐渐减退还有一个重要原因是,夫妇得到了孩子,使他们不能再只关注彼此。在孩子独立离开家庭后,一些失去的浪漫感觉又会重新出现,父母可以重新关注彼此。

促进亲密关系的因素

依恋

爱情不仅是一种选择的体验,也是一种生物性驱使。我们的归属需要具有适应性意义,合作促进我们种族的生存和基因的传递。婴儿期对成人的依赖增强了人类间的联系。与他人的亲密依恋关系构成了一个人生活的核心,人们都是通过这些亲密依恋关系来获得力量和享受生活的。

所有的依恋都有一些共有因素,双方的理解、提供和接受支持、重视并享受和相爱的人在一起。当然激情之爱还会有些其他特征:身体上的亲昵、排他性的期待以及对爱人的强烈迷恋。依恋在婴儿期还可以再细分为三种大类:

  • 安全性依恋,大约七成以上,在母亲在场时能舒适地玩耍,母亲离开后变得紧张,母亲回来时跑向母亲并继续玩耍。这种婴儿在成人后会更容易接近别人,不会太过依赖或被抛弃而苦恼。这样的恋人也更容易将关系维持在令人满意和持久的状态。
  • 回避型依赖,大约两成,这类婴儿在和母亲分离或重逢时,虽然有内部的生理唤醒,但极少表现出悲伤。这类婴儿成人后往往会回避亲密关系,并对这种关系表现出较少兴趣,并倾向于摆脱这种关系。他们更易涉及一夜情,而较少有爱情。
  • 不安全型依赖,约一成。在陌生环境下,这类婴儿会充满焦虑地粘在母亲身边。母亲离开后会哭泣。母亲回来后,他们会表现出冷漠和敌意。成年后,焦虑——矛盾型人格使他们对别人不够信任,产生较强的占有欲和忌妒心。他们与同一个人的关系会反复出现破裂的情况。

这种早期依恋方式也能形成内部工作模式,或关于人与人之间相互关系的独特思考方法。观察发现,敏感、反应型的母亲会让孩子对世界的可靠性形成基本信任感,有利于培养安全型依恋的孩子。

公平

吸引的公平原则:你和你伴侣从感情中所得应该和你们投入的成正比。处于长期的公正关系的人不在乎短期的公正。对他们来说,这些只是“社交债务”的产生和偿还。不斤斤计较是友谊的标志。

对公平的知觉也很重要。处于公平关系的人往往满意度更高,知觉到的不公平可以预测婚姻紧张和对婚姻生活的不满意度。知觉到不公平是一个正反馈过程:觉得不公平的一方会更加沮丧和苦恼,从而加剧感受到不公平。

自我表露

在美满婚姻或亲密友谊中,信任会取代顾虑,使我们更容易展现自己,而不必担心失去友情和爱情。这一特点表现为自我表露

被他人挑选为表露对象通常是很令人高兴的事。相反,如果缺乏发展这种亲密关系的机会,我们就容易感到孤独的痛苦。对于那些我们期望与之有更多交往的人,我们会更多自我表露,且安全型依恋的人会比其他类型自我表露更多。而且表露间存在表露互惠效应,一个人的自我表露会引发对方的自我表露,我们会对那些敞开胸怀的人表露更多,但是亲密关系的发展也不会立即而来(否则就显得不够谨慎和可靠)。它更像跳舞,我表露一点,你表露一点,一次不要太多,有一个持续的相互回应的过程。亲密关系的增加会创造很强的激情感觉,到亲密关系稳定后,激情就相对较小。

有些人——主要是女性——特别善于使人“敞开心扉”,她们可以轻易地引发他人亲密的自我表露。这类人似乎都是好的倾听者。心理学家Rogers称这种人为“促进成长”的听众,表露自己情感,倾听他人情感。

这样的自我表露有助于我们扔掉面具,表现真实的自己——培植爱情的方式。对他人敞开心扉可以是人们间的交往更加愉快。经常敞开心扉的夫妻或情侣会报告更高的满意度且保持更长久的爱情。两个自我相互联系、相互倾诉、相互认同;两个自我保持个性,又共享很多活动,为彼此的相同之处感到愉悦并相互支持——这就是爱情的精髓。

亲密关系如何结束

人们将自己不满意的婚姻关系和想象中可从别处获得的支持和情感相比,选择离婚的人越来越多。

离婚

相对于集体主义中,结婚更意味着承担责任;个人主义者则因为“我们彼此相爱”。个人主义者期待婚姻中有更多激情和个人的自我实现。那些结婚时就已考虑成熟且打算长相厮守的人,确实会有更健康、稳定而长久的婚姻。

符合下面条件的夫妇通常不会离婚:

  • 20岁后结婚
  • 都在稳定的双亲家庭里长大
  • 结婚前谈了很长时间恋爱
  • 接受过较好且相似的教育
  • 有稳定收入
  • 居住在小城镇或农场
  • 结婚前没有同居或怀孕过
  • 彼此间有虔诚的承诺
  • 年龄、信仰和受教育水平相似

分离过程

对于深入和长久的依恋关系,离开是一个过程,而不是一个事件。Baumeister & Wotman的报告显示,在数月或数年后,拒绝别人的爱,比被拒绝更能够唤起更多痛苦。痛苦中来自伤害别人的内疚,对心碎恋人的执着的不安,也来自不止如何反应。

在婚姻关系令人痛苦时,有下面4种反应:

类型 被动的 主动的
建设性 忠诚:等待改善 表达:试图改善关系
破坏性 忽略:无视对方 退出:结束婚姻关系

健康的婚姻不见得没有冲突,而是夫妻双方能够调和差异。痛苦和争吵并不能预测离婚(几乎所有夫妇都经历过冲突)。真正能预测婚姻危机的因素是冷漠、希望破灭和无助

相反,婚姻成功的夫妻有时能从沟通训练中获益,学会避免恶意侮辱,平息怒火,不讲冲突矛头只指向个人。幸福的夫妻间会减少抱怨和责难,增加肯定和赞同,腾出时间表达彼此观点,每天一起祈祷和休闲,并以此改善关系。类似地,持久的凝视等模仿相爱的行为能够激发爱情。Sternberg认为,通过扮演和表达爱意,最初的浪漫和激情能够发展成持久的爱情。

在现代生活中,亲密而持久的婚姻关系正在减少。人们需要付出努力才能防止爱情减退。爱情是在内心决定去爱一个人并对其做出长相厮守的承诺;爱情是可以经营的,它需要相爱的人共同去培育。

利他

利他主义(Altruism):自私自利的反义词。

产生原因

从观察角度和解释层次上,大致分为下面3类:

理论 解释层次 外在帮助 内在帮助
社会交换 心理学 帮助的外在回报 内疚->帮助的情感补偿
社会规范 社会学 互惠规范 社会责任规范
进化理论 生物学 互惠 亲缘选择

获得回报,避免惩罚

包括社会交换理论在内的几种理论都认为,帮助行为可以使施与者和接收者共同受益,并产生社会性商品的交换——爱、服务等。

社会交换

帮助行为的有些回报是外部的,比如,商人捐款以获得良好形象,让顺路人搭车已获得赞赏。我们会最热心地帮助对我们有吸引力渴望得到其赞许的人。当然,给予对方情感支持,也会让自己产生积极心境。志愿者行为也能有益于成年人的精神状态甚至健康状况。

这种收益分析看起来有些有失身份,难道我们没有“纯粹”的利他行为吗?斯金纳在1971年对帮助行为的分析提到,“只有当我们不能解释别人做好事的原因时,我们才会因此信任他们。只有当我们找不到外在解释时,我们才会把行为归因于他们内在的品质而不是外部原因”。

内部回报

很多时候,帮助行为也会来自内部原因,比如助人者的情绪状态或个人品质。接近一个痛苦的人,我们也会感到痛苦,从而实施帮助也会减轻自己的痛苦感。痛苦仅是一个例子,除了痛苦外,内疚也是我们想尽量摆脱的消极情绪。实验显示,人们会尽其所能消除内疚感,减少不良感受,恢复自我形象

  • 违规行为会引发被试的负罪感,从而倾向于帮助行为,将功补过
  • 说了谎的被试更乐意无偿付出一点时间提供帮助

我们在犯错后的行善愿望表明,我们既需要减轻个人的内疚感,也需要恢复动摇了的积极的自我形象。即使我们的内疚感是他人所不知的,我们也会用行动来减轻它。除了利他行为外,内疚感还能促使人们坦白、道歉,避免再犯错误,还会使人更敏感,增进亲密关系。

除了内疚感,消极情绪也对利他行为有影响,但有趣的是,儿童和成人的影响正好相反:

  • 消极情绪减少儿童的帮助行为
  • 消极情绪促进成人的帮助行为

Cialdini等人推测,利他行为带来自我满足和内在回报这一认知来自社会化过程,所以在成人身上很明显,而儿童却没有体现,虽然儿童也有共情能力。实验结果和“人生来自私”的观点不谋而合,孩子们随着年龄增长,学会站在他人角度看待问题,帮助行为也随之发展起来。不过,个别消极情绪例外,如愤怒极度的悲伤的人会处于强烈的自我关注时期,从而抑制了对他人的付出。只有注意力被引到他人的被试才认为帮助别人特别有意义。

和消极情绪相反,快乐的人更乐于帮助别人。这一点不论在儿童还是成人身上都是如此。因为帮助行为能够维持好的心境,同时积极的心境又会产生积极想法和饱满的自尊。快乐的门槛很容易达到,从害怕到轻松的心境也很容易引发帮助行为,如被误诊、或误开罚单。不过随着快乐的心境逐渐消逝,助人性也随之降低。

社会规范

这里说的规范即社会期望。人类社会中一个普遍的道德准则是互惠规范,即对于曾帮助我们的人,我们应该予以帮助,而不是伤害。这种支持性的联系,信任和合作行为保证了团体的正常运作。当人们不能给予回报时,他们可能会因为接受了援助而感到受威胁和被贬低。因此,骄傲、自尊心强的人通常不愿意寻求帮助。

另一条重要的社会规范是社会责任规范,即人们应该帮助那些需要帮助的人。一些实验表明,即使帮助者不为人知,或不能期待任何回报,他们也会帮助那些需要帮助的人。至于如何鉴别“需要帮助的人”,归因很重要。如何一个人穷困潦倒是因为自身原因(懒,不道德),社会规范会让他们自食其果;如果相反是因为环境原因,他们就会得到关怀和帮助。这一规范使人们帮助那些最需要帮助和最应该得到帮助的人。

女性在被知觉为更柔弱和更具依赖性上,会得到男性更多的帮助。而女性对不同性别的求助者则一视同仁。男性会更多地帮助那些外表有吸引力的女性,而不是那些外表不具吸引力的人。女性不仅特定场景下能获得更多帮助,她们在身体和精神上也需求更多帮助。

进化心理学

我们的基因驱使我们采用让其存活能力最大化的生活方式(来自《自私的基因》观点),亲缘保护和互惠就是。亲缘选择让我们偏袒那些和自己拥有相同基因的人,我们的子女和亲兄弟间有天然的基因相似性,所以可以说帮助近亲是我们的本性。亲缘保护还决定了种族内的群体偏好,这也是导致诸多冲突的根源。

生命体帮助他人,也是因为它期待着得到回报性的帮助。不作出互惠行为的个体则会被抛弃和排斥。互惠在小的、与外界隔离的群体中更好地发挥作用(可能因为相互信赖更强)。因此,互惠行为在偏远乡村比在大城市发生得更多。互惠特征能保持至今有一个进化的原因:群体间竞争时,相互支持、互惠的群体比不互惠的群体能维持更长时间。

真正的利他

上面我们从内在(避免痛苦)和外在(获得回报或逃脱惩罚)分析了利他的动机,似乎还存在着由共情引发的真正的利他行为,即帮助别人的意愿不仅受利己考虑影响,也会来自无私的考虑。当我们感到和某人有关联时,就会产生共情。这也是为什么从众中提到个性化的受害者更能引人共鸣。实验发现,共情被唤起的人通常会施与帮助。但是如果同时知道有别的方式能让我们好受些,我们就不太可能帮助别人

巴特森和其同事认为共情导致的利他行为有利有弊,缺点之一是会引起偏爱、不公正的帮助行为,并对广泛的公共利益冷漠。

何时会提供帮助

在场人数、个人情绪、价值观念、时间紧迫度都会影响帮助行为。

在场人数

在更多人在场的情况下,受害者较少有机会得到帮助。在旁观者增多时,任何一个旁观者都会更少注意到事件发生,更少把它解释为紧急情况,更少认为自己有采取行动的责任。

注意

在群体人数较多时,群体中的个人会较少注意紧急情况的发生。

解释

如从众一章中所说的“信息影响”,在对环境不够熟悉时,每个人通常会以他人行为作为现实情况的线索。另一方面,这种错觉又被透明错觉所助长,透明错觉指出我们通常会高估他人了解我们内心状态的能力。因为,我们对自己情境的关心会来的更明显,对自身情绪也更敏感,所以会以为他人能很容易发现。在这两种错觉的叠加下,我们往往会忽视或误解需要帮助的线索。

对行为的解释很重要,一个尝试打开车锁的人到底是小偷还是被锁在门外的人,我们对其的解释会影响我们的反应,帮助抑或报警。陌生人之间的暴力也会得到更多的干预行为。

责任归属

人群变大时,帮助情境会变模糊。相反,当紧急情况很清晰时,群体中的人提供帮助的可能性就远小于独处的人。这也在一定程度上解释了,为什么城里人通常比乡村人不愿意帮助别人,在大城市因遇到需要帮助的人太多,会产生“同情疲劳”和“感官超载”。另外,责任扩散也是原因之一,群体中个人感觉到的责任被分散。

在一项实验中,面对面的被试由于有表情交流,要比背对背的被试更容易提供帮助。相对相互孤立的群体,相互联系的群体会提供更多帮助。

他人的示范作用

亲社会的榜样可以促进利他行为。目睹令人感动的善举,常常会引发一种升华的状态:胸腔被温暖和激情膨胀的特殊感,令人们战栗、流泪、喉咙抽紧。孩子不仅听从耳濡的教诲,也从目染的行为中学习道德观。

时间压力

时间充裕且认为自己的角色无关紧要的被试会放弃事务停下来提供帮助。

相似性

上一章提到,相似性引起喜欢,我们也更多地对那些跟我们相似的人产生共情。在实验中,被试对和自己有相似特征的面孔更信任和慷慨。相似性也会引起同种族的偏爱行为,不过,由于很少有人希望明确表现出偏见,所以实验中这种偏爱倾向没那么明显。

谁会提供帮助

  • 人格特征。具有较积极情绪,较高共情能力和高自我效能感的人更易提供帮助。不过助人性的个体特征没那么明显,需要和特定情境结合在一起分析。当受助者是陌生人且情境有危险性时,男性更常伸出援手。而在安全的场景下,如志愿者或花时间的陪伴,女性更乐意一些。尤其是基于亲密关系的关心而不是帮助陌生人。
  • 宗教信仰。在长期帮助上,如志愿者工作和慈善捐款,宗教信仰有更好的预测性。在各种社会团体中,宗教团体的成员和公民参与的各种形式有最紧密的联系。

如何增加帮助行为

根据上面对帮助行为的分析,可以从个人角度和社会角度去做。

个人角度

去除对利他行为的抑制。减少模糊性,提高责任感。个人化的方式能使人感到不是匿名的,因而有更高的责任感,任何能让旁观者变得凸显个人特征的事情——个人请求、目光接触、告知姓名、会面预期——都增加了帮助的可能性。同时,自我意识更高的人更经常将理想付诸于实践。

另一方面,可以引起内疚和对自我形象的关心留面子(door-in-the-face)是利用前者的手段,先提出较过分的请求,在被拒绝后,利用拒绝者的内疚提出想要的请求通常会被满足。比如,研究者先提出一个非常大的请求——承诺为不良儿童做为期两年的无偿咨询,在被全部拒绝后,从侧面切入:“好吧,如果你不愿意,这点小事你愿意帮忙吗?”。Cialdini和Schroeder提供了引发个体关注自我形象的方法:请求非常微小的帮助,以至于不好意思被拒绝。因为此时拒绝,通常证明自己的自私。

社会角度

  • 教化道德包容,避免道德排除,利他主义社会化的第一步就是去除天然的内群体偏爱
  • 树立利他主义榜样,亲社会的电视榜样所起的所用远大于发社会榜样的作用
  • 归因为利他主义动机,给予一种行为超过适度的反馈时,个体可能将行为归于奖励这一外在动机。在没有报酬和潜在社会压力时,如果答应帮助别人,会产生最强的无私感。但这不意味着不能有褒奖,意外的褒奖能令人感到胜任和有价值,需避免的是过度的滥用的奖励。助人行动能够促进把自己看作“富有同情心和乐于助人的人”,而这种知觉又反过来促进了进一步的帮助行为
  • 习得的利他主义,即在了解上面这些会影响帮助行为的因素后,人们就会意识到群体情境对帮助行为的一些抑制作用,从而去避免

冲突和和解

冲突是被知觉到的行为或目标的不相容。无论冲突的双方能否正确感知对方的行为,他们总会认为一方的获益就是另一方的损失。但从另一方面来看,冲突体现了参与、承诺和关心。在能够被理解和解决的情况下,冲突可以促进人际关系的变化和发展。从积极意义上讲,和平是创造性处理冲突得到的结果。

冲突是如何产生的

冲突来自于多种方面,实际上,不论是人与人的,还是国与国的冲突,其产生原因都是大体相似的。

社会困境

社会困境即个人利益和集体利益相互冲突,在个体追逐自己的私利时,避免不了地损害了集体的或其他团头的利益。囚徒困境公共地悲剧就是两个典型案例。

囚徒困境即,两个囚犯有守口如瓶和指认对方两种选择,对于个人来讲背叛对方总能得到更好的结果,但实际上双方都选择对个体更差的策略时,能获得更大的收益。囚徒困境有一个限制条件: 双方只做1次决策,所以没有长远考虑的机会。军备竞赛等囚徒困境的实际案例频繁地发生在现实生活中,无条件地信任他人和采取合作态度反而会让自己陷入被动。

公共地悲剧即个人对公共资源的过度滥用,比如共同放牧的草原等。在经济学角度讲,一个人对公共资源的使用减少了他人的使用,这种负外部性使每个人对公共资源的成本评估较低,从而造成滥用。这也是为什么,当一种资源未获得明确分配时,人们往往不自觉地消费更多

当然也有人,如亚当·斯密认为,从某一方面,由于每个人都试图通过努力使自己利益最大化,这一过程也会使整个社会产值达到最大。

有一些手段可以缓解这种个人利益和集体利益的冲突:

  • 设立管理条例
  • 设置税和管理费等,提高人们的评估成本
  • 缩小群体规模,在一个较小的团体中,人们能更明确地感到自我责任和自己对集体的影响,更容易从集体获得满足感。从而更为节制。因此,也有些政治理论家和社会心理学家建议将公共资源划分为较小的单位。
  • 在小范围内的公共资源,可以采用沟通的方式,开放、坦诚而明确的交流可以消除不信任,产生合作行为
  • 在社会范围倡导利他规范,当合作行为可以明显增进公共福利时,个人就更可能表现出符合社会规范的行为

竞争

竞争是个人和个人之间极为常见的引发冲突的因素。竞争可以加强知觉到的差异,并引发更多的攻击行为。在谢里夫的山贼洞夏令营实验中,分胜负的竞争活动带来了激烈的冲突、对别组成员的歧视、组内强烈的团结意识和集体荣誉感。另外,群体极化又会加剧冲突。

在以下两种情况下,竞争更易于引发冲突:

  • 人们知觉到诸如金钱、工作岗位等资源时有限零和
  • 一个明显成为潜在竞争者的外部团体

知觉到的不公正

每个人对于公平的定义都不同,大体上可以分为根据付出分配根据需要分配(共产主义)。

在实验中,人们通常不会主动要求对自己或自己所在团体给予优待,但是当他们接受更多好处时,往往会欣然接受,并相信他们获得的就是他们应得的。占了便宜的个人和小组,有的会产生负罪感,从而引发道歉或提供补偿,有的则通过贬低他人的付出来缓解自己的罪恶感。对于那些利益受损者,他们或接受并认同,或寻求其他补偿,或通过报复来获得心理平衡。

有意思的是,一个人越是对自我投入评价越高,就越会感到怀才不遇且意图报复。女性更经常视自己和男性平等时,她们的相对剥夺感也越强。

误解

本章开头介绍冲突概念时,特意强调了“被知觉到”一词。实际上,由于下面一些因素,我们经常有误解的不一致性。

  • 自我服务偏见会高估自己做的好事,却不为自己做的坏事负责任
  • 自我合理化让人们倾向于否认自己的错误
  • 基本归因偏差使冲突的双方都把对方的敌意行为归因于他们邪恶的品质
  • 群体思维会在群体中固化利己、自我合理化和群体偏见,从而形成难以改变的刻板印象

在冲突双方的眼里,对方的形象都是被扭曲的:

  • 把自己的目标看得最重要
  • 为“我们”感到骄傲,贬低“他们”
  • 坚信自己利益被损害
  • 强调对集体的热爱、团结和忠诚
  • 鼓励自我牺牲,压制批评

镜像知觉

在冲突中,扭曲的误解可以达到惊人的一致性,如美苏冷战中双方国民对对方国家的印象。在冲突被双方知觉到时,至少有一方对另一方存在误解,伴随自我证实的预言,双方的敌意程度会在恶性循环中加深。这种负面的镜像知觉很多时候会成为通往和平的障碍——冲突的一方认为和自己持相反立场的攻击者的攻击动机显示了他们嗜血的本性,而与自己信仰相同的攻击者则不过是自卫或还击而已。

津巴多指出,冲突会将世界一分为二,即好人和坏人,非黑即白,冲突中对立的双方常常夸大这种差异。换位思考可以缓解这种趋势,但绝非易事。激化冲突的另一错误观念是“领导邪恶——民众善良论”,即领导阶级是邪恶的,他们控制和操纵着善良的民众。在台独、港独问题上就能很容易发现这点。

简单化思维

冲突形势越紧张,理智的思考变得困难。对敌人的看法也会变得更简化和刻板,如经验式的判断。在大战爆发、军事冲突、革命前,发起攻击的领导者会表现出愈加简化的思维方式,有个原因是这样更有助于传达给群众,和让群众理解。

知觉转化

误解程度会伴随冲突程度起伏变化。比如国家间的关系也会影响国民对其他国家的认识和观念。

从上面可以发现,在我们能够抛开偏见,并解决实际存在的分歧时才能结束冲突。一个忠告是,在冲突中不要认为别人和你在价值观和道德上格格不入,进行换位思考。

获得和平

上面提到冲突因何而起(社会困境、激烈竞争、知觉到的不公正、误解),解决冲突也有4个建议,简称4C:

  • 接触,contact
  • 合作,cooperation
  • 沟通,communication
  • 调和,conciliation

接触

在紧张程度不是很高时,可以通过接触增进相互了解,以期获得和平。

  • 接近性和曝光效应使我们更喜欢接触更多的人
  • “行为验证态度”让我们暗示自己,达到缓和效果

以种族隔离为例,种族接触可以减少种族歧视,实验发现,在种族以外,接触也能够预测偏见的减少。但是,情况不总是这样,很多国际学生交流项目并不能使学生对居住国产生所预期的积极影响。接触也需要有一些其他限制条件配合才能改善种族态度,如友谊平等接触

和其他群体建立的友谊的人,往往容易对这些群体产生积极的态度,这种情感纽带降低了焦虑。友谊是成功接触的关键。另外,接触需要是平等的,如果接触是竞争性的,或不平等的,那么结果反而会恶化,比如主人和仆人,狱警和囚犯。反例则是同事、同学、邻居等。种族多样化引起的非正式班内互动可以促进致力提高和对差异化的更大程度接受。

合作

共同的外部威胁超级目标可以建立起内部团结和合作。

民族间的冲突会提高民族自尊心,在战争时期面对一个明确的外部威胁时,我们的群体归属感会极度高涨,小布什在911时期的民众支持率从51%飙升到90%。因为这一点,甚至有时候,领导人会刻意创造出一个假想敌来提高民族凝聚力,奥威尔《1984》里的老大哥就是这么做的。在现实生活中,一个共同的强大的敌人也是一种强大的凝聚力量,《三体》中三体星人的入侵就带来了全人类的空前凝聚。

另一种凝聚力量是超级目标,即集中力量办大事。谢里夫实验中,最后通过修复水管、解决骑车抛锚等问题,又降低了团员们的敌意,使他们变回了朋友。成年人和谢里夫实验中的小男孩是一样的。一起工作对分解小团体,建立一个更大的更具包容性的团体很有用。当然,合作需要是成功的,失败的合作反而会恶化冲突。

由于人们天然的社会性,我们的自我概念中除了我是谁,还包括我们的社会同一性,即我们是谁,能强化自我概念和自豪感,我们“是谁”也暗示了我们“不是谁”。内群体偏见有时会造成冲突,并在群体极化下演变成暴力行为,比如球迷间的斗殴。伴随成功,人们的群体认同也会高涨,电竞战队获胜会为它带来更多粉丝。为了促进“内群体一致性”,很多学校都强制要求身着校服,消除小群体竞争,增进合作。

除了上面两例,合作性学习也能够改善个体间关系,减少冲突,改善种族态度。相对于传统的、充满竞争的学校里学习的学生,在学习参加混合民族学习小组的学生,有更好的种族态度。合作性学习有助于建立更亲密接触,产生户主和支持的关系。“拼图”式学习即把学生分为小组,拆分任务为小部分,组内每个学生各自承担一部分,自行学习后将学习成果教授给其他成员,共同完成对学习材料的学习。其中每个小组成员都会感受到自己的必要性,这种相互帮助的气氛也使成员对同伴更加喜爱。

上面提到的共同敌人、超级目标和合作性学习总结起来就是,为实现一个目标而进行平等的接触

我们每天都在处理各种各样的社会身份,我们的自豪感需要我们用户更广泛群体、民族的身份认同。在种族多元化的文化中,尤其是历史并不悠久的国家中,种族身份和国家身份同时存在,人们需要平衡它们之间的关系。有时需要通过提出一致的理想来推广公民身份的认同,如美国的“合众为一”。

沟通

沟通也可以分为几种形式:谈判调解仲裁

谈判并不总能达到双赢的结果,有时迟到的协议会造成双输的后果

第三方调解人可以让冲突双方做出让步并挽回面子。调解人需要让他们暂时放弃冲突中的自身需求,把非输既赢变成“双赢”。克制的沟通可以减少自我证实的误解,有时建设性地解决冲突比保持沉默能带来更多和谐

那么如何界定建设性的争吵呢:

  • 不要过早道歉
  • 不要保持沉默,或逃离现场
  • 不要引入无关话题
  • 不要人身攻击
  • 不要假装同意
  • 要私下争吵,避开他人
  • 要清晰界定问题
  • 要接受对自己的反馈
  • 澄清自己的态度,直面问题
  • 提问以引导对方表达其观点
  • 等待对方平静下来
  • 提出双方都满意的建议

冲突达到建设性结果,一定程度建立在信任的基础上。双方互不信任时,就需要第三方调解的介入。调解人需要建立一种情境,帮助双方理解对方,并感到被对方理解。中立的第三方还可以提出双方都接受的建议。

如果冲突连调解都无法解决,就需要仲裁来给出一个确定方案,大多数的争论者都会陷入过度乐观,即相信仲裁结果对自己有利。

和解

和解是双方面的,单方的妥协让步是行不通的。Charles Osgood提出了GRIT方案:逐步(Graduat),互惠(reciprocat),主动(initiative)地减少紧张(tension reduction)。GRIT要求一方在宣布希望调和的愿望时,做出一些小的意在降低冲突的行为。和解行动并不需要发起者在某一领域做出大的牺牲,但是要保持“坚定、公平、友善”。在实验室里进行的长期的两难实验中,“投桃报李”策略被证明是成功的(《自私的基因》中也有提到)。

和解行动可以使双方从紧张的台阶下来,使接触、合作、沟通重新变得可能。

流水账向,记念总是充满未知和惊喜的旅程。

准备

计划旅行总是件头疼的事情,尤其是没有人可以帮你分担的时候。因为时间限制排除了青海湖骑行和西南游行后,最终目的地确定为某凯10年前去过的丽江。另一方面,某凯领导身先士卒地因加班倒下,这次中秋佳节某凯得以顺利加入我的出游计划,可喜可贺。

Day 1: 出发

丽江的天气预报都是骗人的

路线:北京T2航站楼 -> 昆明三义机场 -> 昆明站 -> 丽江站 -> 大研古城

起床.jpg

由于北京往返云南的航班实在有限,来回的旅途不甚方便。为了赶早上8:50去昆明的飞机,我又回到了当初学科目二的起床时间5:20。

昆明机场.jpg

幸运地是,飞机没有延误,让我顺利地在14:30前从昆明机场赶到了昆明站,甚至还抽出了半个小时去了个德克士。

动车开至大理时,天气便阴沉起来,山区里的雾从视野边缘沿着山势蔓延而下。在山区中穿行的火车从远处看一定甚为壮观。

火车外.jpg

丽江站位于市郊边缘(比机场好点),虽然公交仅需1元,但是苦于没有零钱,只能打滴滴到大研古城。到附近的九洲饭店时,几乎和下午3点出发的某凯同时到达。对于普通游客来说,住宿上有两个选择:大研古城束河古城。个人比较推荐大研,位于城区内,交通食宿方便。

客栈.jpg

来云南怎能不尝下野生菌火锅,还未煮熟,鲜味就伴着蒸汽飘了出来。店家特意叮嘱煮沸后5分钟再吃,应该也是怕我们中招。另外,这家店的酥肉一点不输北方。

野生菌火锅.jpg

丽江和大理气候很湿润,四季如春,夏天最热30度出头,冬天最冷也在10度以上,但是昼夜的温差较大,早晚要注意保暖。着装上,长袖长裤正常春秋装即可。丽江下雨没个准儿,天气预报可信度不高,建议随身带伞。来丽江前,一周都是雨的天气预报还是挺让人绝望的。幸运的是,最终几天里只有第一天晚上下了雨。

Day 2: 雪山

吸氧?不存在的。

路线:甘海子 -> 蓝月谷、白水河 -> 冰川公园 -> 登顶 -> 大研古城

玉龙雪山和周边的景点很值得一逛。不过有几点对于第一次来这里的游客不大友好:

  • 线上购票入口不透明,可以在丽江旅游集团公众号上购买,大索道票价140元,购买时需要选择时段,每天有限额,卖完即止。强烈建议提前购买。除了索道外,进入景区还要另收取80元门票,可以在游云南微信公众号上购买,入园时要出示购票二维码。
  • 需要准备防寒服和氧气瓶。丽江城区海拔2500m,雪山园内海拔3000m+,索道顶海拔4500m。体质较弱的人可能会有高原反应,更何况在索道顶上还有数千级台阶爬到4680m的最高点(并不是山顶)。另外山顶阴晴不定,气温在10度以下,需要防寒防雨服。但是防寒服和氧气瓶的租用收费也不大透明,古城里和山脚下都有租的地方,价格不一。
  • 园内景点相距很远,雪山索道、蓝月谷、云杉坪、牦牛坪之间必须坐摆渡车来往。坐车的地点指引不明显,没有人带很容易懵逼。

山.jpg

个人很推荐报一日游的小团前往,网上搜到的都是纯玩团,6-7人成团,价格从200+到300+不等。司机会帮你完成上面不透明的订票、防寒服/氧气租用、景点接驳指引等环节。甚至还包含午餐(但是要注意午餐时当地人推荐的当地特色,比古城里卖的贵很多)。

蓝月谷1.jpg

一般的路线会先路过甘海子。甘海子是一片高原草甸,平时没有起雾时能看到牦牛吃草。由于我们上午出发时,有个团员迷路,到达园内的时间稍晚。安排上就先去了蓝月谷、白水河。蓝月谷是雪山下的一条自然河流,水的蓝色来源于里面的铜离子。配合山景颇有感觉。

蓝月谷2.jpg

吃完饭,下午开始爬山,爬玉龙雪山必须要坐大索道才能到达冰川公园,索道起点3306m,海拔在4500m,10分钟的时间爬升近1000m。整个爬升过程可见到山上植被从松柏到草甸再到山岩再到冰川的过渡,手里的相机根本停不下来。山间阴晴不定,有时恍如置身仙境,不知被云还是雾包裹,对面索道上的缆车也仿佛穿梭云间。

索道缆车.jpg

索道顶便是登山步道起点,登山步道到最高处爬升100m,到4680m处,出于保护目的,无法到山顶。栈道两旁遍是风化的山岩,稍往上走,就能看见宏伟的冰川,瞬间就明白“玉龙”二字的由来。由于是淡季,栈道上的人并不多,都身着防寒服,边吸氧边向上爬。说到吸氧,个人体验青壮年男性1瓶即可,我和某凯1人1瓶下山后还吸了几天都没吸完……

雪山1.jpg

雪山2.jpg

不过一会儿,山上雨势便大。我们几人乘索道返回甘海子坐上返回古城的面包车,由于疲惫,一行几乎无言。

一个酒店.jpg

古城2.jpg

我们订的民宿位置精妙,位于狮子山半山腰,下午回来后就顺带逛了下古城。大研古城属于开发得比较彻底的,城区里面商业气息很是浓厚。不过配合着狮子山的地形以及玉河的水势,复古又现代的石巷还是颇有特点。许是我一时喜欢喧闹,天色渐晚,灯火通明人声熙攘又让古城的商业化没那么可憎。

古城1.jpg

晚饭吃的是土鸡米线 + 鸡蛋牛奶醪糟,座位就在河边,不时有凉风拂过,不失为一种惊喜。

醪糟.jpg

Day 3: 古城

吃一堑长一智

路线:大研古城 -> 黑龙潭公园 -> 束河古镇 -> 酒吧

原定计划是去虎跳峡,虎跳峡是长江的一小段,分三部分:上虎跳、中虎跳、下虎跳。其中上虎跳开发比较完善,有两个成规模景区,分别位于江的两岸,由丽江和香格里拉开发。其中丽江侧游客较少,体验稍好。中虎跳、下虎跳适合徒步多日玩耍体验。逼逼叨叨这么多,最终得知,最近(9月13日)赶上雨季,危险系数太高,景区不开门,计划成功泡汤。

狮子山1.jpg

腾出的一天,领略了白天的大研古城和稍显青涩的束河古镇。古城内的大型景点并不多,狮子山木府算是两个。前者门票35,后者门票40,不算太贵。狮子山是木府的后山,是丽江城区内的制高点,在万古楼顶楼可以纵览古城新城的任何角落,以及边缘的群山,甚是过瘾。

狮子山2.jpg

木府是明清时期丽江地区统治阶级木氏土司政府所在地,也是《鹿鼎记》的故事发生地。

木府万卷楼.jpg

白天的古城撇去游客,但看小巷、流水、房檐,在配上肉眼可见的流云,还是另有感觉。

大研古城1.jpg

随便拍上一张,若是好好PS一番,就是一副大片。

灯笼.jpg

番外篇:狗

古城里的狗,大多以最舒服的姿势趴在地上,对外物无甚兴趣。

狗1.jpg

狗2.jpg

狗3.jpg

狗4.jpg

狗5.jpg

在去午饭的路上品尝了6.6元的奶茶,茶味很浓,蛮有特色。

奶茶.jpg

随后沿着玉河水系上溯,景区外的玉河两岸更是安静祥和。午餐品尝了黑山羊火锅,顺便见识了粑粑和纳西族月饼(疑似)。两人面对大众点评优惠三人餐,毫无疑问地败下阵来。

纳西族月饼.jpg

饭后就便逛了黑龙潭公园,公园是市民公园的性质,有山有水,可以来抱着放松身心的心态来逛逛,后山属于健身路线,较少有游客涉足,且要登记身份避免走失。本身不收门票,但需要检查古城维护费收据,1人50元,如果之前在其他景点交过,展示证明即可。如果从公园北侧小口进入,则没有人查验(我们从北门出去后路过才发现……)。有意思的是,在公园最北端有个小湖,颜色和蓝月谷的别无二致。

黑龙潭公园.jpg

番外篇:谜之健身器材

园内有一处年久失修的健身器材,很是抽象。光看造型,完全无法联想其使用方式。

器材1.jpg

器材2.jpg

器材3.jpg

器材4.jpg

束河古镇较远,需要打车前往。一般在南门落客。一天内特定时间段(游客较多的时段)需要买门票。在吸取了狮子山和黑龙潭的教训后,我们发现有些门票是可以合理避过的。从南大门右边的小路和左边停车场、派出所进入古镇不检查门票。

风铃.jpg

和大方华丽的姐姐大研相比,束河像是还不经世事的妹妹,刚学会化妆,便往脸上一顿浓妆艳抹。束河古镇里的路较宽,拐弯都比较规规矩矩,水流除了西边一条河外,镇里的河略窄。也许是位置较偏的缘故,游客比较少。除了四方听音飞花触水等几个小广场外,较少有游客聚集。

四方听音.jpg

北侧的九鼎龙潭放生桥处的水极清澈,令人想捧起一抔洗把脸。

九鼎龙潭.jpg

对比之下,南边哈里谷区域却一片荒芜,店门紧缩,院内一片荒草,原本酒吧街内的河流或干涸或绿得浑浊。除了一些拍婚纱照的新人和玩耍的本地小孩外,甚至连游客都没有。

哈里谷1.jpg

不敢想象,夜幕降临后这里会是什么样。曾经,这里繁华的时候,若都是酒吧,人流来往,处处欢歌,还有流水桥廊还有楼上路上买醉的人,应该是很美妙了。只可惜如今蚊蝇滋生,死寂一片。

哈里谷2.jpg

古镇里卖银、买玉的手艺人很多,工匠街一带更多,可能是淡季外加游客有限,才下午五点街上就没了人烟。

百度地图误我,又逛了鸡肋都比不上的景点后,我们打车回到了大研。

丽江古城里的清吧染上景区的风格,都浮夸无比。似乎没个驻唱就无法开店(虽然事实很可能是这样)。甚至有的酒吧没有鸡尾酒的选择,啤酒配上这种氛围,我怎么想怎么感觉怪怪的。好歹也是农历八月十五,我们在大众点评上找了一个看似靠谱的酒吧就杀了过去。

物价虚高,没有零食,花生24元一碟,不续,诚意稍显不足。有驻唱算是有点氛围上的弥补(私心觉得如果是jazz、民谣风格的就更妙),要求不能太高。只可惜,这里喝啤酒和看风景的客更多,歌手几次互动,响应者寥寥。真是不解风情。

酒.jpg

穿插着各种交流感情,几杯下去,微醺。挺好。

Day 4: 拉市海

路线:拉市海 -> 邮局 -> 网吧

由于假期有限,规划在丽江的3天半里就没有计划泸沽湖,第4天就去了相对较近的拉市海,拉市海湿地近年已不让在湖里划船,湖边的相关设计占比就多了起来。去拉市海的公交一样比较麻烦,所以我们还是报了半日游的团。行程较为简单:骑马 + 吃饭 + 湖边拍照。

只可惜骑马的造型不大雅观,只敢自己珍藏。路上牵马的纳西族马夫倒是挺有意思。他看上去60出头,格外健谈,也许是和游客呆多了,俏皮话和段子也是一个接一个。侃天之余还说起之前在藏区的当兵经历,感慨国家进步之快和维尼的英明,疯狂吐槽影帝时期种种不满。中间还教了了我们几个纳西族的短语。

自拍.jpg

大研附近的邮局有限,古城内的邮局在深处,民主路上的一个较容易找到,下午寄完明信片方才3点。等好不容易拿了首胜从网吧走出时,已晚上八点半。晚上的烤小瓜(西葫芦)是个惊喜。

小瓜.jpg

Day 5: 大理

云原来也能这么好看

路线:洱海公园 -> 海心亭 -> 奥林匹克广场

囿于穷,回北京得在大理转机,幸得大理一日走马观花。在大理火车站附近顺便点了一些特色菜:饵丝大救驾水性杨花。后两个店家没有了,不过饵丝是用大米做的和米线口感较像的面食,不过更细。大救驾貌似是腾冲名小吃,用饵块做的。“水性杨花”则是丽江的一种水生植物。别的不说,饵丝看上去还挺不错,对我口味。

饵丝.jpg

明珠广场顶上可以看到洱海南面的大片城区,不过被洱海公园的山挡着看不到洱海。

明珠广场.jpg

洱海公园是免门票的市民公园,园内的小型动物园虽比较鸡肋,但猴山却格外有意思,整个后山面积很大,里面猴子、猫、飞鸟和谐相处,蛮有趣味。

猴山.jpg

临到山茶园,远处的苍山有乌云越过山脊向下蔓延,山和云形成的层次感甚为壮观。

苍山.jpg

在山茶园的风花雪月亭向下望,渔家女雕像后的洱海一览无余,只教人心情舒畅。

风花雪月亭.jpg

渔家女雕像.jpg

雕像所在的广场上,可以坐在海边尽情享受湖光山景,气温不高不低,常有微风左右相伴,真是片伊甸园般的景象。

洱海1.jpg

洱海2.jpg

时间还多,我们一直跨过洱河走到了蝴蝶形状的奥林匹克广场方才兴尽而止。有时间,真应该上苍山、大理古城里去看看。

奥林匹克广场.jpg

Day 6: END

每一次旅行都是一种经历,感受异地的生活节奏、自然风光和人文风情。这次的行程虽然稍显短促,但也带着满满的伴手礼和记忆踏上归途。同时,还解决了2个困扰我数月的难题。

飞机上,云层之下的洱海显得更为壮观。

飞机上的洱海.jpg

–END–

php.net

简介

PHP来源于工程PHP/FI,由Rasmus Lerdorf创建于1995年,起初只是一套简单的Perl脚本,名字叫做“Personal Home Page Tools”,语法也和Perl很像,随着用户的增加,改进为用C语言实现。1997年,Andi Gutmans 和 Zeev Suraski 重写了代码,推出第三版,PHP/FI也演变成PHP(PHP: Hypertext Preprocessor)。注意,这是一个递归的缩写。

1999年,由两人改进的更具模块化的“Zend Engine”引入PHP,在结合了许多新功能后,2000年5月发布官方版PHP 4.0。如今广泛使用的5.x版本从2004年起发布。5.x版本支持完整的面向对象模型。目前的最新版本已经到了7.x版本(直接从稳定的5.6版跃迁)。

由于丰富的PHP主要用于服务端的脚本程序,就像其他的CGI程序,如收集表单,生成网页,发送/接收Cookie等。除此以外,PHP还用于命令行脚本,编写桌面应用程序。这两种开发可能会用到PHP的拓展库。由于解析器的存在,PHP的跨平台能力很好。

关于php的绝大多数内容都可以在php.net上找到,上面介绍的历史也是如此。本文的绝大多数内容更是如此。

安装和配置

在通常情况下,php用于服务器端脚本,安装配置较之Javascript复杂很多。在Unix环境下,假设服务器环境(如Apache, Nginx等)已经安装完毕,可以通过configure脚本安装配置。Windows环境下,通过MSI文件安装配置PHP和所有内置以及PECL拓展库。此外Mac OS X,云平台等安装各有不同,详见官方教程

配置文件(php.ini)在PHP启动时被读取,作为服务器模块版本的PHP,仅在服务器启动时读取1次,作为CGI和CLI版本,每次调用都会读取。用户亦可自定义自己的user.ini文件。PHP的有些指令可以在PHP脚本中用ini_set()设定,有些只能在php.inihttpd.conf中设定。这些是由指令的模式决定的,模式有4种:PHP_INI_USER, PHP_INI_PERDIR, PHP_INI_SYSTEM, PHP_INI_ALL。具体见文档

PHP作为Apache模块运行时,还可以用php_value, php_flag, php_admin_value, php_admin_flag命令设置。

第一段代码

与C等语言通过代码输出HTML不同的是,PHP页面本身就是HTML,你也完全可以像通常建立HTML页面那样创建和编辑PHP页面,只不过其中嵌入了<?php?>包裹的PHP代码。与Javascript不同的是,PHP运行在服务端,用户无从得知脚本是如何运行的。

值得一提的是,除了上述的开始和结束标记,使用<script language ="php"></script>或者asp风格的短标记<?=, <%=也行(不建议)。在这一对标记之外的内容都会被PHP解析器忽略。可以在脚本中通过phpinfo()打印php的整体配置信息。

1
2
3
<?php
phpinfo();
?>

在html语句中嵌入php语句时,尽量做到将业务逻辑和展示语句隔离开对维护php工程有着极大的好处。

特性参考

PHP标识

如上文中提到,PHP通过<?php?>分隔php脚本,在php.ini激活short_open_tag配置后,支持使用短标记作为分隔符。如果文件内容为纯PHP代码,最好在文末删除结束标记,以免打印意料之外的空白。如下示例:

1
2
3
4
5
<?php
echo "Hello world";
// ... more code
echo "Last statement";
// stop here

PHP使用分号作为分隔符,支持C,C++,Perl风格的注释。即///**/#

类型 & 类型转换

PHP的原始数据类型有boolean,integer,float,string,array,object,resource,NULL。其中前4种为标量,第5,6中为复合类型。resouce表示资源,NULL表示无类型。PHP中float也称为double。在确保代码易读性上,还有mixed,number和callback几种伪类型。需要注意的是,PHP和Javascript一样,类型往往根据上下文确定。

boolean

和JavaScript类似。

只有TRUE或FALSE,除了false以外,还有下列假值:

  • 0
  • 0.0
  • ‘’
  • “0”
  • [],
  • {}(仅4.0)
  • NULL

其余均为真值(包含任何resource)。和Javascript类似,支持===全等。

integer

有十进制,十六进制,八进制,二进制表示。除十进制外,分别以0, 0x, 0b开头。5.0.5后最大值可以用常量PHP_INT_MAX设置。整数溢出时会被解释为float注意:八进制中传递非法数字后,后面数字会被忽略。类型转换时,可以使用intval()。在浮点数过大,分数强制转换和其他类型转换时,结果未定义。

float

又称为double和real,支持科学记数法。运算时精度有限,高精度要求下参考任意精度数学函数和gmp函数。在比较大小时需要谨慎,可以采用相减之差和最大容忍度比较的方法作折衷。常量NAN表示浮点计算中不可描述的值,为float类型,不等于任何其他变量,甚至自身。可以用is_nan()检查。

string

和JavaScript区别较大。

  • PHP字符串的字符占1个字节,因此不支持Unicode。字符串最长可达2GB
  • 表示字符串有4种方法,单引号,双引号,heredoc和nowdoc
    • 单引号下,只转义单引号和反斜线,其余字符均为plain text,支持多行;
    • 双引号下,对换行回车制表符等特殊字符进行转义,还会对变量解析($xxx)的形式(和Javascript相似)。
    • Heredoc结构里,在<<<符号后提供一个标识符然后换行,接下来是字符串本身,字符串后另起一行用前面定义的标识符作为结束标志。中间内容的处理方式同双引号。
1
2
3
4
5
6
<?php
$str = <<<EOD
Example of string
spanning multiple lines
using heredoc syntax.
EOD;

5.3.0以后,可以使用heredoc结构初始化静态变量和类的属性以及常量。nowdoc结构类似于单引号版的heredoc,但是跟在<<<之后的标识符要用单引号括起来,多用在不解析特殊字符的大段文本中。在双引号或heredoc结构中,变量会被解析,简单语法下,PHP解析器会去组合尽量多的标识形成一个合法的变量名。复杂语法下,$符号的外侧或里侧会紧贴{},来实现更复杂的变量表达式。

字符串中的字符可以用[]或者{}(不建议)访问。下标超出字符串长度时,会将多出的长度用空格填充。另外,字符串使用.连接。使用strval()转换变量为字符串,boolean会转成"1"""。integer和float作字面转换。**array总转换成"Array"**。object总转换成”Object”。NULL总转变成""

serialize()可以串行化大部分PHP值。字符串转为数值时,类似Javascript的parseInt()/parseFloat(),试图从头转换直到遇到不合法字符,支持科学记数法。区别在于PHP中失败时返回0而不是NAN

关于string的更多介绍,参加官方文档String一章。

array

1
2
3
4
5
6
7
8
9
10
11
<?php
$array = array(
"foo" => "bar",
"bar" => "foo",
);

// 自 PHP 5.4 起
$array = [
"foo" => "bar",
"bar" => "foo",
];

与Javascript区别较大,PHP中的数组也是个有序映射,描述了keys到values的映射。array使用array()初始化,在5.4版本后支持字面量定义。key可以是integer或string(integer时是数组,string时是键值对),value可以是任何类型。使用[]访问和修改数组元素,通过unset()删除某键值对(类似与Javascript的delete)。有趣的是,使用[]不指定键名时,则取当前最大整数索引值(曾经存在即可),新的键名在之上加1。可以使用array_values()重建索引。

转换为数组时,除object、NULL类型外,其余类型得到只有一个元素的数组。object类型转换时,单元为对象的属性,键名为成员变量名,还有其他特殊情况见文档数组部分。NULL会转换为一个空数组。

1
2
3
4
5
6
7
8
9
10
<?php
class A {
private $A; // This will become '\0A\0A'
}
class B extends A {
private $A; // This will become '\0B\0A'
public $AA; // This will become 'AA'
}

var_dump((array) new B());

object

对象,通过new来实例化一个类产生。转换为对象时,PHP会创建一个内置类stdClass的实例,可以通过new stdClass()创建一个空对象。php 7后,还有new class{}(object) []方法。

resource

用于保存到外部资源的一个引用,通过专门的函数建立和使用,由Zend引擎维护资源回收。

NULL

表示一个变量没有值。可细分为被赋值为NULL,尚未赋值和被unset()NULL不区分大小写

callback

类似Javascript中的function类型,一些函数如call_user_func()可以接收用户定义的回调函数作为参数。传递时,以string类型传递函数名。5.3.0后可以直接传递closure给回调参数。

其余伪类型多用于代码的说明注释中,如mixed表述多种不确定类型,void表述函数返回值无用或不接受任何参数等。

类型转换

使用var_dump()查看值和类型,gettype()查看类型,is_int/is_string/…判断类型,(type)settype()强制类型转换。PHP的强制转换和C非常相似。目前支持(int), (bool), (float), (string), (array), (object), (unset)(转换为NULL)。5.2版本后支持(binary)转换。

除了强制转换,PHP中会根据需要对变量自动转换,如加法。与Javascript的+不大不同,PHP会优先将操作数转为float,否则会将操作数解释为integer。数组的键名会优先转换为integer(仅十进制),再转换为string。下面就是一个有趣的例子:

1
2
3
4
5
6
<?php
$foo = "0"; // $foo is a string
$foo += 2; // $foo is an int now
$foo = $foo + 1.3; // $foo is a float now (3.3)
$foo = 5 + "10 Little Piggies"; // $foo is an integer (15)
$foo = 5 + "Small Pigs"; // $foo is an integer (5)

变量 & 常量

PHP变量以$符号开头,只能包含数字字母(这里说的字母包含ASCII字符)和下划线且不能以数字开头。变量区分大小写。$this是特殊变量不能赋值。可以在$前加&符号引用赋值,在改变原变量时,目标变量也会改动。isset()可以检查变量是否已被赋值。

1
2
3
4
5
6
7
8
<?php
$var = 'Bob';
$Var = 'Joe';
echo "$var, $Var"; // 输出 "Bob, Joe"

$4site = 'not yet'; // 非法变量名;以数字开头
$_4site = 'not yet'; // 合法变量名;以下划线开头
$i站点is = 'mansikka'; // 合法变量名;可以用中文

用户在为变量命名时,有几点要注意的。function, class, interface, 常量和函数外定义的变量会进入全局命名空间;建议在函数名中用_区分,类名中用驼峰或首字母大写的驼峰命名。注意:很多情况下,PHP会自动将变量名中的点转换成下划线。

可变变量

PHP中的变量名可以很方便地改变,而且可变变量可以用在数组或对象中,如下面的例子,。使用可变变量时,注意通过花括号给属性名清晰定界。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// Given these variables ...
$nameTypes = array("first", "last", "company");
$name_first = "John";
$name_last = "Doe";
$name_company = "PHP.net";

// Then this loop is ...
foreach($nameTypes as $type)
print ${"name_$type"} . "\n";

// ... equivalent to this print statement.
print "$name_first\n$name_last\n$name_company\n";

预定义变量

PHP提供许多预定义的变量。PHP中的许多预定义变量都是“超全局的”,这意味着它们在脚本的全部作用域都可见。这种类型在4.1版本中被引入,有$GLOBALS, $_SERVER, $_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, $_REQUEST, $_ENV。它们在5.4版本后不能作为函数的输入参数。通过这些预设的超全局变量,PHP可以轻松地获取请求的各种参数。

除了上述超全局变量外,还有$php_errormsg, $HTTP_RAW_POST_DATA(使用php://input代替), $http_response_header(使用HTTP包装其时,该变量会被自动填充),$argc$argv分别代表传递给脚本的参数数目和参数数组(运行在命令行下时)。

作用域

变量作用域通常为文件作用域。函数内部的声明的变量被限制在函数作用域内。同时,和Javascript相同,PHP没有块级作用域。注意,PHP中定义全局变量需使用global关键字。在函数内部,变量优先视作局部变量。下面的脚本不会有任何输出,因为echo引用了一个局部变量$a,但是在函数作用域内它并没有被赋值。要想$a在函数作用域内可见,需要在引用前声明global

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a = 1; /* global scope */

function Test() {
echo $a; /* reference to local scope variable */
}
Test();

function Test2() {
global $a;
echo $a;
}
Test2();

静态变量通过static声明,仅在局部作用域存在,程序离开作用域时内容不丢失。静态变量不能使用表达式初始化。在下面的例子中,函数仅在第一次调用时初始化$a变量,之后每次调用都会输出$a,并加一。

1
2
3
4
5
6
7
<?php
function test()
{
static $a = 0;
echo $a;
$a++;
}

常量

在类外,常量通过define(name. value)函数定义。在类内使用const定义常量(5.3.0后)。常量的命名规范同C。常量只能是标量。在访问常量值时,名字前不带$。常量名事先无法确定时,使用constant()获取常量。常量没有作用域的限制,可以在任何位置访问。

PHP定义了大量的魔术常量,都以两个下划线开头和结尾。有__LINE____FILE__, __DIR__, __FUNCTION__, __CLASS__, __TRAIT__, METHOD__, __NAMESPACE__。具体解释见官方文档

表达式 & 流程

PHP的表达式根据官方的定义表述,是任何有值的东西。表达式的组成类似于其他语言,从略。值得注意的有以下几点:

  • PHP的逻辑运算符同时有&&, ||以及and, xor, or两套,但是后一套的优先级最低
  • PHP提供@作为错误控制运算符,放置在表达式前可以忽略产生的任何错误信息。强烈不建议使用
  • 反引号``执行其中的shell命令,并将输出结果返回,等同于执行shell_exec()
  • +, ==, ===还可以用于数组间的运算,进行数组的连接,键值对相同的检测。
  • instanceof用于确定变量是否属于某个类的实例。用法如$a instanceof MyClass

算法流程上,PHP类似C风格。不同点有:

  • 提供在<?php>闭合标签内使用for endfor这种用法
  • foreach(array as $key => $value)便于遍历数组。(注意:在$value&可以在foreach循环中改变value的值)
  • break可以接受一个可选的数字决定跳出几层循环
  • continue接受一个可选的数字参数来决定跳过几重循环到循环结尾。默认值是1
  • declare设定一段代码的施行指令,目前只支持ticksencoding。前者控制执行计时的若干条命令后的操作,后者决定代码的运行编码。
  • requireinclude效果类似,用法同C,它们也有带后缀_once的操作符

函数

PHP的函数定义和其他语言类似,定义的函数都具有全局作用域。不同的是

  • PHP可以使用create_function(args, code)这样的函数定义函数(类似JS中的new Function()
  • 函数需要先定义后使用(这个只是与Javascript不同)
  • PHP可以定义有条件的函数,通过用if包裹和放在function定义内

和C/C++风格很像的是:

  • PHP函数参数接收的是一个复制,需要传递引用改变原值;
  • 支持默认参数,需放在最右;
  • 5.0之后支持对输入参数类型检查,到5.4为止支持class / array / callable类型,7.0以后支持标量类型。如果给出的值类型不对,那么将会产生一个错误
  • 7.0之后支持不对输入参数强制类型转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class C {}
class D extends C {}

// This doesn't extend C.
class E {}

function f(C $c) {
echo get_class($c)."\n";
}

f(new C);
f(new D);
f(new E);

和Js相似的一点时,5.6版之后支持使用...符号获取参数列表。

和可变变量一样,PHP中有可变函数,用法和可变变量一样。在调用对象的静态方法时,函数调用要优于静态属性,下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Foo
{
static $variable = 'static property';
static function Variable()
{
echo 'Method Variable called';
}
}

echo Foo::$variable; // This prints 'static property'. It does need a $variable in this scope.
$variable = "Variable";
Foo::$variable(); // This calls $foo->Variable() reading $variable in this scope.

5.3之后,PHP也支持匿名函数,并可以传递给一个变量储存。实际中,这种表达式会被转换为内置类Closure的对象实例。闭包可以从父作用域继承变量,但是此类变量需要用use结构传递进去,类似于function() use($a){}这样的形式。

类 & 对象

PHP承袭着面向对象语言对类和对象的处理。类以class开头,里面包含属性和方法等,可以包含自己的常量。通过new实例化,通过extends实现继承。子类使用parent::访问被覆盖的属性或方法,使用self::自身的静态属性和方法。5.5之后使用ClassName::class可以获取带有命名空间的完整类名。轻量级的类可以通过强制转换关联数组实现。

类中的静态属性通过::访问,非静态属性通过->访问。定义常量时使用const,常量的值必须是一个定值(5.6之后可以是数学运算结果)。PHP 5新增了关键字final,修饰方法或者类不可被继承。

PHP 5中,**__autoload()函数会在使用未定义的类时自动调用**,5.3.0之后通常使用spl_autoload_register()作为autoload的替代。__construct()__destruct()分别是构造和析构函数,5.3.3之前,在没有__construct()函数也没有父类时,会寻找命名空间中与类名同名的方法。

1
2
3
4
5
6
7
<?php
spl_autoload_register(function ($class_name) {
require_once $class_name . '.php';
});

$obj = new MyClass1();
$obj2 = new MyClass2();

trait & 匿名类

在访问控制,继承,抽象类,接口等方面PHP和传统的面向对象语言很像。在5.4.0后,PHP提供了trait作为类之间代码水平复用的特性(很像mixin)。在class定义中使用use来获取trait,类似interface,一个类可以插入多个trait,trait会覆盖基类方法而被当前类方法覆盖。在多个trait的同名方法发生冲突时,通过insteadofas来决定使用哪个,具体见trait文档。trait的功能使用依赖注入也可以完成,相关讨论见stackoverflow trait practivestrait vs interface。trait甚至还支持抽象成员和静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
<?php
trait A {
public function smallTalk() {
echo 'a';
}
public function bigTalk() {
echo 'A';
}
}

trait B {
public function smallTalk() {
echo 'b';
}
public function bigTalk() {
echo 'B';
}
}

class Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}

class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}

PHP7.0之后支持匿名类,用于创建一次性的简单对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// PHP 7 之前的代码
class Logger
{
public function log($msg)
{
echo $msg;
}
}

$util->setLogger(new Logger());

// 使用了 PHP 7+ 后的代码
$util->setLogger(new class {
public function log($msg)
{
echo $msg;
}
});

“重载”

PHP提供的重载(overload)语义和其他大部分OOP语言不同,指在调用当前环境下未定义或不可见的类属性或方法时调用重载方法。PHP借助魔术方法实现重载。读写不可访问属性时,__get()__set()分别被调用;对不可访问属性调用issetunset时,__isset()__unset()分别被调用。属性重载只能在对象中进行。调用不可访问的方法和静态方法时,__call()__callStatic()分别被调用,方法重载用法类似属性重载。重载的示例见文档。(不建议使用这个特性,这会影响ide补全和代码的可读性)。

PHP5提供foreach方法遍历对象,默认情况可见属性都会被遍历,可以让类实现Iterator接口从而自行决定如何处理遍历。实现IteratorAggregate接口可以代替实现所有的Iterator方法,IteratorAggregate只需实现IteratorAggregate::getIterator()方法即可。

魔术方法

PHP将所有__开头的类方法保留为魔术方法__sleep()方法在serialize()函数前调用,应返回一个包含对象中所有应被序列化的变量名称的数组,相对的__wakeup()在反序列化函数前调用。__toString()在把一个类视作字符串时怎样回应时调用。__invoke()在把一个类视作函数调用时调用。更多方法见魔术方法页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;

public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}

private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}

public function __sleep()
{
return array('server', 'username', 'password', 'db');
}

public function __wakeup()
{
$this->connect();
}
}

PHP使用clone创造对象的浅复制(即只创造属性的引用),魔术方法__clone()在clone完成后调用。PHP 5中的对象甚至可以相互比较,使用==判断属性和属性值是否一致,===判断变量是否是同一个实例。

自5.3.0起,PHP增加了static::关键字和后期静态绑定的功能,用于在继承范围内引用静态调用的类。静态环境下绑定静态方法可以让子类在自己的环境下(自己的this)调用继承自基类的方法。这种方式绑定非静态方法时,会出现不同结果,尽量避免使用。

对象通过serialize()unserialize()来序列化和反序列化一个对象,对象的方法和静态成员不会保留。在解序列的文件域内需要包含类的定义

预定义接口

PHP预定义了许多接口。Traversal接口监测一个类是否可以使用foreach进行遍历(仅供引擎使用)。Iterator接口用来实现对象的foreach迭代,有rewind, current, key, next, valid等成员方法。除此以外还有聚合迭代,数组式访问,序列化,生成器接口等接口和Closure类。这里从略。

命名空间

命名空间是PHP一个比较有特点的特性。在PHP中用命名空间解决类库和用户代码名字冲突的问题。实际上命名空间所做的事情就是代码模块化,正如Java的packages和Javascript里CommonJS规范一样。命名空间的命名方法类似变量,不允许使用PHP或php开头的命名空间。下面是一个命名空间的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace my\name; 

class MyClass {}
function myfunction() {}
const MYCONST = 1;

$a = new MyClass;
$c = new \my\name\MyClass;

$a = strlen('hi');

$d = namespace\MYCONST;

$d = __NAMESPACE__ . '\MYCONST';
echo constant($d);

除了declare语句以外,namespace的定义需在文件的最前面。PHP与其它的语言特征不同,同一个命名空间可以定义在多个文件中,即允许将同一个命名空间的内容分割存放在不同的文件中。在命名空间中使用define定义常量时,需要带上__NAMESPACE__,否则意味着定义在全局空间下。

PHP中的命名空间和文件目录很像,也支持层级化的定义方法,即定义子命名空间,父子间通过反斜线\隔开。可以在单文件内定义多个namespace(不提倡),建议namespace间通过大括号隔离开。PHP命名空间可以和文件系统进行类比,类名非限定时,会在当前空间寻找,以\开头时从全局空间寻找(相对目录),否则从当前空间起向下寻找(绝对目录)。下面是一个使用了三种方法的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
namespace Foo\Bar;
include 'file1.php';

const FOO = 2;
function foo() {}
class foo {
static function staticmethod() {}
}

/* 非限定名称 */
foo(); // 解析为 Foo\Bar\foo
foo::staticmethod(); // 解析为类 Foo\Bar\foo的静态方法static method
echo FOO; // 解析为常量 Foo\Bar\FOO

/* 限定名称 */
subnamespace\foo(); // 解析为函数 Foo\Bar\subnamespace\foo
subnamespace\foo::staticmethod(); // 解析为类 Foo\Bar\subnamespace\foo, 以及类的方法 staticmethod
echo subnamespace\FOO; // 解析为常量 Foo\Bar\subnamespace\FOO

/* 完全限定名称 */
\Foo\Bar\foo(); // 解析为函数 Foo\Bar\foo
\Foo\Bar\foo::staticmethod(); // 解析为类 Foo\Bar\foo, 以及类的方法 staticmethod
echo \Foo\Bar\FOO; // 解析为常量 Foo\Bar\FOO

命名空间的装载和名称的解析是在编译期完成的。命名空间有三种定义方法:

  • 非限定名称:名称中不包含命名空间分割符,即\,如Foo
  • 限定名称:名称中包含命名空间分割符,如Foo\Bar
  • 完全限定名称:名称中包含命名空间分割符,且以\开始的标识符,如\Foo\Bar

PHP支持使用namespace关键字或__NAMESPACE__魔术常量获取当前所在命名空间。所有支持命名空间的PHP版本支持三种别名或导入方式:为类名称使用别名为接口使用别名为命名空间名称使用别名。这么做类似于在操作系统中创建符号连接。别名通过操作符use as实现。注意:导入命名空间后文件内的类名,接口名等会收到导入的影响。在一个命名空间中,当PHP遇到一个非限定的类、函数或常量名称时,它使用不同的优先策略来解析该名称。对于函数和常量来说,如果当前命名空间中不存在该函数或常量,PHP会退而使用全局空间中的函数或常量。因此在访问系统内部或不包含在命名空间中的类名称时,必须使用完全限定名称。

错误和异常

PHP的错误类型有很多,可以见类型列表。PHP对错误的汇报方式由php.ini中的error_reporting命令控制,可以在运行时通过error_reporting()函数动态修改。在开发环境,建议将级别设置到E_ALL,同时在脚本的开头设置级别。php.ini中的display_errors指令控制是否将错误显示在脚本输出中,建议在生产环境中关闭。log_errors指令控制错误记录。

PHP 5中异常可以被抛出,由try/catch语句块获取。catch获得的是一个Exception类的实例。类似Java,可以在catch后加上finally语句块。Exception是一个类,有getMessagegetTraceAsString等方法可以使用和拓展,详见介绍。PHP 7中,大多数错误都被作为Error异常抛出,可以被第一个匹配的try/catch语句块捕获,否则交给PHP相应的异常处理函数处理,如果尚未通过set_exception_handler()注册童永刚异常处理函数,则会报告一个Fatal Error。注意:捕获错误或异常时,若在自定义命名空间下,Exception需要用完全限定方式书写。

7.0以后的版本中,ErrorException同属于Throwable类型,这一点与5.x版本不同。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//To catch both exceptions and errors in PHP 5.x and 7, add a catch block for Exception AFTER catching Throwable first.
//Once PHP 5.x support is no longer needed, the block catching Exception can be removed.
try
{
// Code that may throw an Exception or Error.
}
catch (Throwable $t)
{
// Executed only in PHP 7, will not match in PHP 5
}
catch (Exception $e)
{
// Executed only in PHP 5, will not be reached in PHP 7
}

生成器(generator)

PHP中的生成器的概念与Java等高级语言中生成器的概念无二。生成器函数看起来像一个普通的函数,不同的是普通函数返回一个值,而一个生成器可以yield生成许多它所需要的值。当一个生成器被调用的时候,它返回一个可以被遍历的对象。PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态。生成器不可以返回值。return语句只会终止生成器继续执行。

yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。可以使用yield返回键值对,引用或NULL等。在PHP 7以后,使用yield from可以从实现了Iterator接口的对象或使用yield的函数中yield值。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4];
yield from new ArrayIterator([5, 6]);
yield from seven_eight();
return yield from nine_ten();
}

function seven_eight() {
yield 7;
yield from eight();
}

function eight() {
yield 8;
}

function nine_ten() {
yield 9;
return 10;
}

$gen = count_to_ten();
foreach ($gen as $num) {
echo "$num ";
}
echo $gen->getReturn();

对比生成器和实现Iterator接口的类来看,生成器的代码可读性更高,代码量也更少,缺憾在于不能多次迭代和回退,除非重建或使用clone。

引用

PHP的引用意味着不同的名字访问同一个变量内容,通过在变量前加上&使用。在对一个未定义的变量进行引用参数传递或引用返回时,会自动创建该变量。不使用&符号时,意味着生成一个拷贝。

在进行引用传递时,只能传递变量,New语句和函数中返回的引用;引用返回时,需要在函数名前加上&符号,同时接受返回值的变量也需写为接收引用的形式。
通过unset销毁引用,销毁引用的同时不会销毁原变量(类似于删除符号链接)。global $var实际上就是创建了到$GLOBALS[]的引用。$this也是同理。

支持的协议

PHP带有内置URL风格的封装协议,可用于类似fopen()copy()file_exists()filesize()的文件系统函数,如file, http, ftp, php, zlib, data等。其中php://提供的是输入输出流和错误描述符的访问能力。创建数据流前,可以通过stream_context_create()创建上下文选项,定义数据流的选项。

函数参考

PHP本身提供了海量的函数。且都可以全局访问到。

内核

内核部分的函数,不能通过编译选项去除。PHP这部分的函数和介绍相当多,这里只撷选了常用的部分。

数组

这部分的函数主要用来进行和数组相关的操作,由于PHP中的数组包括了键值对这样类似对象的功能,函数的数量很多,甚至有些冗余。

  • array_chunk 将数组分割为多个,单元数目由size决定。返回一个多维数组。
  • array_merge 将多个数组的单元合并在一起,字符串键名相同时,后面的值会覆盖前一个。类似的还有array_merge_recursive。
  • array_count_values 统计数组中所有的值出现的次数,返回一个关联数组
  • array_diff 计算数组的差集,返回在array1但不在array2的元素
  • array_intersect 计算数组的交集,返回一个在array1中出现同时也在其他所有参数数组中出现的值。在差和交的名称前加上u的函数可以自己指定比较方法。
  • array_fill 用给定的值填充数组的num个条目,start_index为返回数组的第一个索引值。array_fill_keys函数可以填充键值对。array_pad 用值将数组填充到指定长度。键从第一个整型数开始,否则从0开始。
  • array_flip 返回一个交换键和值的数组。不合法的值将不会反转。类似的array_reverse返回一个单元顺序相反的数组。
  • array_combine(array $keys , array $values)返回一个由keys数组作键,values数组作值的新数组,两个数组长度不一样时抛出异常。array_keys(array $array [, mixed $search_value [, bool $strict= false ]])返回所有值为search_value的键名,strict表示是否进行严格比较。类似地,array_values(array $input)返回一个由所有值组成的数组,并建立起数字索引。
  • array_multisort用来一次多多个数组排序,输入数组被当作一个数据表的若干列来排序。常用在对数据库数据的排序。返回值为bool类型。
  • array_pusharray_pop分别在array的末尾弹出或压入一个元素。
  • array_shiftarray_uinshift完成类似于上面的功能,不过是在数组开头。
  • array_product()array_sum()分别返回数组的乘积和总和。
  • array_filter用回调函数过滤数组单元。没有回调函数时将删除input中等值于FALSE的条目。
  • array_map返回一个arr1所有单元经过callback作用后的单元。callback 接受的参数数目应该和传递给 array_map() 函数的数组数目一致。
  • array_walk使用用户自定义的函数对数组每个函数做回调处理。
  • array_reduce根据回调将array简化为一个值。function变量可以读取result和item。
  • array_replace(array $array1 , array $array2 [, array $... ])将前面的数组的键值对覆盖为后面的键值对。多维数组下有recursive版本。
  • array_key_exists检查键名是否存在于数组中。array_search(mixed $needle , array $haystack [, bool $strict = false ])在数组中搜索给定值。
  • array_slice根据offset和length从数组中取出一段。
  • array_splice把input数组中由offset和length指定的单元去掉,如果提供了replacement参数,则用其中的单元取代。
  • array_unique用于移除数组中重复的值

除了这些,还有is_array()explode()split()等不以array开头的函数和数组相关,大多用来进行一些简单的操作,列表见数组参考。和数组排序相关的函数也有很多,它们在排序依据,是否稳定等方面各不相同,更多内容参考对数组进行排序。

字符串

和字符串相关的函数也很多,但只有以str开头的是严格意义上的字符串函数。下面列举了部分:

  • addslashes转义字符串中的单引号,双引号,反斜线和NUL
  • chr返回ASCII码对应的字符,ord()是其互补函数
  • chunk_split($body[,int $chunklen = 76 [,string $end = "\r\n" ]] )拆分$body$chunklen的小块,每块后用$end结尾
  • crypt返回一个单向字符串散列,md5计算字符串的MD5散列值,sha1计算sha1散列值
  • echo 输出一组字符串
  • explode使用一个字符串分割另一个字符串;类似地,implode将一个一维数组的值转为字符串。又写作join
  • htmlentites转义所有的特殊字符为HTML实体;html_entity_decode()实现相反的步骤。
  • htmlspecialchars()htmlspecialchars_decode()完成的功能和上面相似,但是转义的字符只有&"'<>
  • lcfirst将首字母小写,ucfirst将首字母大写,ucwords将每个单词的首字母大写
  • ltrimrtrimtrim删除字符串首部,尾端和两端的空白。
  • str_getcsv解析csv字符串为一个数组
  • str_pad使用另一个字符串填充字符串到指定长度
  • str_repeat 重复一个字符串
  • str_replace 字符串替换,preg_replace的特殊情况
  • str_shuffle随机打乱一个字符串
  • str_split将一个字符串转换为数组
  • strstr查找字符串的第一次出现。stristr()则不区分大小写地查找
  • strcmp二进制安全字符串比较大小,strncmp类似,不过允许指定比较的长度,strnatcmp以自然顺序比较字符串
  • strlen获取字符串长度
  • strpos查找字符串初次出现位置,strrpos查找最后一次出现,strripos不区分大小写查找最后一次出现
  • strrev反转字符串
  • strip_tags去除str中的空字符,HTML标记和PHP标记,和fgetss()机制一样
  • strtoupper将字符串转化为大写,strtolower将字符串转换为小写
  • strtr()翻译、转换指定字符
  • substr返回字符串的子串
  • substr_count返回子字符串在字符串中出现的次数。

变量

  • boolvar()转换变量为bool类型
  • empty()判断变量是否为空
  • isset()检测变量是否已设置
  • intval()获取变量整数值,floatval()获取变量浮点数值,strval()获取变量的字符串表示
  • get_resource_type()获取资源类型
  • gettype()获取变量类型,settype($var, string $type)设置变量类型
  • is_array, is_bool, is_callable, is_float, is_int, is_null, is_numeric, is_object, is_resource, is_scalar, is_string用来检测各种类型。
  • print_r()var_dump()打印变量的相关信息,var_export()以合法PHP代码的形式返回变量的字符串表示
  • serialize()序列化一个变量,unserialize()反序列化一个变量
  • unset()销毁指定的变量

类和对象

  • spl_autoload_register()尝试在类名未定义时启动类的自动加载
  • class_alias为一个类创建别名。
  • class_exists检查指定的类是否定义
  • get_class()返回对象实例所属类的名字。类似地还有get_class_varsget_class_methods函数。
  • get_declare_classesget_declare_interfaces以及get_declare_traits获取脚本中已定义的类、接口、trait数组。
  • method_exists( mixed $object , string $method_name )检查类方法是否存在于指定object中,类似地还有property_exsitsinterface_existstrait_exist

日期和时间

PHP中的时间以64为数字存储。使用时需要配置好php.ini中时区等信息。DateTimeDateTimeZoneDateInterval等对象便于进行相关的操作。PHP同时提供了OOP风格和过程化风格两种方式使用函数。其中DateTimeDateTimeImmutable都继承自DateTimeInterface接口,有着diffformatgetTimestampgetTimezone等方法。

DateTime中的部分方法如下:

  • add(DateInterval $interval)在当前时间上加上一个时间段
  • sub(DateInterval $interval)在当前时间上减去一个时间段。
  • __construct(),创建一个对象,过程化风格: date_create()
  • createFromFormat创建一种时间格式format的写法格式见参考
  • modify修改当前时间,modify为合法的时间格式。
  • setDate(int $year, int $month, int $day )设置日期
  • setTime(int $hour ,int $minute [,int $second = 0 ])设置时间。
  • setTimestamp()设置时间戳。

上述方法都有对应的过程化风格的对应函数。

DatePeriod和DateTimeZone等的介绍从略。除了以上的对象方法过程化的函数外,还有以下一些常用方法:

  • date($format[, $timestamp),格式化一个本地时间
  • getdate(),获得日期时间信息,localtime功能类似,返回一个数组。
  • mktime获得一个日期的时间戳,默认为当前。类似的还有timemicrotime,返回一个时间戳类型
  • strtotime将英文文本的日期时间解析为Unix时间戳
  • 有意思的是date_sunsetdate_sunrise可以获取指定时间戳的日出日落时间

文件系统

  • basename()返回路径的文件名部分;dirname()返回路径中的目录部分,realpath()返回规范的绝对路径名
  • chgrp()改变文件所属组,类似的还有chmodchown
  • copy用于拷贝文件,rename用于移动和重命名文件。注意,这里没有delete函数。unlink用于删除文件
  • link ()建立一个硬连接,linkinfo, lstat给出连接信息。symlink创建一个符号连接。
  • mkdirrmdir用来创建和删除文件夹
  • file把整个文件都入到一个数组中,一行一个元素,可以使用URL作为文件名。file_exists检查文件或目录是否已存在。tmpfile则会建立一个关闭后自动删除的临时文件。
  • file_get_contents将文件读入到字符串中,可以使用stream_context_create创建上下文进行更细致的操作。
  • file_put_contents()写文件,和依次调用fopen(), fwrite(), fclose()效果一样。
  • fileatime, filectime, filemtime, filegroup, fileowner, fileperms, filesize, filetype, stat等和字面意义一样获取文件的各方面信息。它们接收文件路径作为参数。
  • is_dir, is_executable, is_file, is_link, is_readable, is_uploaded_file, is_writable检查文件各种属性
  • fopen打开一个文件,返回一个resource句柄,可以交给fread, fwrite, fscanf, fclose等函数做读写操作。
  • fgets从当前指针处读取一行,fgetc读取一个字符,fstat返回文件信息,ftruncate将文件阶段到给定长度。
  • glob()寻找和pattern匹配的文件路径

Directory类通过dir()创建。Directory实例有closereadrewind三种方法。分别用来释放句柄,读取条目和倒回开头。初次以外还有下面这些常用的目录相关函数。

  • chdir(string $directory)用来改变当前目录,
  • getcwd取得当前工作目录
  • scandir()返回一个包含目录中所有文件和目录的数组
  • closedir()关闭通过opendir()打开的目录流。
  • readdir()返回目录中下一个文件的文件名。文件名以在文件系统中的排序返回。

错误处理

下面这些函数允许你定义自己的错误处理规则,以及修改错误记录的方式:

  • debug_backtrace()产生一条PHP的回溯跟踪,返回数组类型;debug_print_backtrace()则将回溯打印出来。
  • error_get_last()获取最后一个发生错误的信息,error_clear_last()清除最后一个错误信息
  • error_log()发送错误信息到web服务器的错误日志或是一个文件里。
  • error_reporting()设置应该报告的PHP错误级别。
  • set_error_handler, set_exception_handler, restore_error_handler, restore_exception_handler分别是设置和重置错误以及异常处理的函数
  • trigger_error()触发一个用户级别的错误条件,在运行出现异常时,需要产生一个特定响应时很有用。

session

在会话支持下,每个访问网站的用户都有一个唯一的id标识,这个标识可以存储在cookie中,也可以通过URL传递。当一个访问者访问网站时,PHP将自动检查(如果session.auto_start被设置为1)或者在你要求下检查(明确通过session_start()或者隐式通 session_register()) 当前会话 id 是否是先前发送的请求创建. 如果是这种情况,那么先前保存的环境将被重建。

安全方面需要注意以下几点:

  • session.cookie_lifetime=0, 即浏览器不持久化存储cookie数据
  • session.use_cookies=On 并且session.use_only_cookies=On,即通过HTTP cookie实现会话ID管理
  • session.use_strict_mode=On,即禁止使用未初始化会话id的会话,从而防止Javascript进行会话ID的注入
  • session.cookie_httponly=On,禁止Javascript访问会话ID
  • session.cookie_secure=On,仅在HTTPS协议下访问session ID,用在仅支持HTTPS的站点
  • session.hash_function="sha256"。 高强度的散列函数可以产生高强度的会话ID

其他注意事项可以在PHP的会话与安全章节找到,根据实际需要选择。下面是一些session函数的使用:

  • session_destroy(), 销毁一个会话里的全部数据,但不会重置相关全局变量也不会重置cookie,再次使用时需要重新调用session_start()函数。为彻底删除session,需要调用setcookie()清除cookie中的session ID。
  • session_cache_expire()设置或读取当前缓存到期时间(这个只和浏览器页面刷新缓存有关)
  • session_id()获取/设置当前会话ID,PHP仅允许会话ID包括a-z A-Z 0-9 ,(逗号) -(减号).如果不是用cookie来存储session ID,session ID通常附在SID常量中,放在URL里。
  • session_regenerate_id()在不修改当前session数据的前提下使用新的ID替换原有会话ID。如果启用了session.use_trans_sid选项,那么必须在调用session_regenerate_id()函数之后开始进行输出工作,否则会导致使用原有的会话 ID
  • session_start()创建新会话或者重用现有会话。 如果通过GET或者POST方式,或者使cookie提交了会话ID,则会重用现有会话。
  • session_status()返回当前会话状态
  • session_write_close()写入session数据,然后关闭会话
  • session_name()设置或返回当前回话名称,名称应短小易懂,且不能由纯数字组成,如website_id
  • session_save_path()读取/设置当前会话的保存路径
  • session_unset()释放当前会话注册的所有会话变量

进程控制

这部分函数提供执行系统本身命令的能力。注意,以加锁方式打开的文件,必须在执行后台程序前关闭

  • escapeshellarg(string $arg)escapeshellcmd(string $command)对参数和命令元字符转义,保证安全。
  • exec()passthru()都能执行一个外部程序,区别是前者返回结果的最后一行,后者返回未经处理的全部输出数据。
  • shell_exec()在shell环境下执行命令,以字符串的形式返回完整的字符串。
  • system(string $command[, int &$return_var ] )执行 command 参数所指定的命令,并且输出执行结果。

函数调用

  • call_user_func(callable $callback [, mixed $parameter [, mixed $... ]])把第一个参数作为回掉函数调用。在参数很多时,建议使用$callback(…values)的形式传入数组。类似的还有forward_static_callforward_static_call_array用来调用静态方法。
  • func_get_arg(int $arg_num)func_get_arg()返回自定义函数的参数和参数列表,用在函数体内。
  • function_exists()判断函数是否定义
  • register_shutdown_function, register_tick_function用来注册exit之后和每个tick后执行的函数

Hash

这部分函数自5.1.2版本后成为核心的一部分。

  • hash()根据指定的哈希算法生成哈希值。类似的还有hash_file
  • hash_hmac()使用HMAC方法生成带有密钥的哈希值,类似的还有hash_hmac_file
  • hash_init()初始化一个哈希运算上下文,返回resource类型
  • hash_update(resource $context , string $data)向活跃的哈希运算上下文中填充数据。细化的,还有hash_update_filehash_update_stream两个函数。
  • hash_final()结束哈希上下文,返回摘要内容。
  • hash_copy()返回一个哈希运算上下文副本。

PHP自身

这些函数允许你获得许多关于PHP本身的参数。

  • assert()检查一个断言是否为FALSE,并在失败的时候调用assert_options()中指定的回调函数
  • dl()运行时加载一个PHP扩展
  • get_cfg_var()获取PHP配置选项的值
  • get_current_user()获取当前PHP脚本所有者名称
  • get_included_files()返回被include和require文件名的 array
  • ini_get()获取一个配置选项的值
  • ini_set()设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复
  • ini_restore()恢复指定的配置选项到它的原始值
  • memory_get_usage()返回当前分配给你的 PHP 脚本的内存量,单位是字节(byte)
  • php_sapi_name()返回web服务器和PHP之间的接口类型
  • php_uname()返回运行PHP的系统的有关信息
  • phpinfo([int $what = INFO_ALL])输出关于 PHP 配置的信息。可以通过what筛选输出内容。
  • phpversion()获取当前的PHP版本
  • version_compare()对比两个「PHP 规范化」的版本数字字符串

数学

这部分函数处理integer和float范围内的计算。预定义常量包括M_PI, M_E, M_LOG2E, M_LN2, M_PI_2, M_1_PI, M_SQRT2, M_SQRT3, INF等诸多数学常量。函数名和其他语言类似。

  • 三角函数相关:sin, cos, tan, asin, acos, atan计算单位为弧度
  • 双曲函数相关:sinh, cosh, tanh, asinh, acosh, atanh, atan2
  • 对数相关:log, log10, log1p
  • 指数相关:pow, exp, expm
  • 近似相关:round, floor, ceil
  • 随机数相关: rand(int $min , int $max), mt_rand(用法同rand,性能更好), srandmt_srand(现已不需要使用)
  • 进制转换相关:bindec, octdec, hexdec转为十进制数(读取字符串,输出数字),相对应还有decbin, decbin, dechexbase_convert()可以做任意进制转换
  • 角度相关:deg2rad角度转弧度
  • 运算相关:intdiv返回商的整数部分,fmod返回浮点数余数。abs计算绝对值,sqrt计算开根号
  • 判断相关:is_finite, is_infinite, is_nan,
  • 其他:max, min(可以输入数组), pi, hypot(根据直角边计算三角形斜边长)

输出控制

PHP脚本有输出时,输出控制函数可以用这些来控制输出。如通过ob_start()将下文的输出放在缓冲区直到调用ob_end_flush()。通常配合header()使用,在真正返回数据前写入header和cookie。

从略。

杂项

  • constant()返回一个常量的值
  • define()定义一个常量
  • exit()输出一个消息并且退出当前脚本,dieexit的同名函数。
  • highlight_file()语法高亮一个文件
  • highlight_string()语法高亮一个字符串,使用方法同上。
  • sleep()延迟指定秒数执行。类似的还有usleep以指定微秒数暂缓执行,time_sleep_until使脚本睡眠到指定时间
  • uniqid()返回一个基于当前微秒级时间的带前缀的唯一ID。

绑定拓展库

下面的拓展库绑定在PHP发行包中。较之内核部分的函数,更偏向为解决某类问题而设计。这里也只摘选部分常用的介绍。

Ctype

用来检测 在当前的区域设定下,一个字符或者字符串 是否仅包含指定类型的字符。根据官方描述,“如果可以满足需求,请优先考虑使用 ctype 函数, 而不是正则表达式或者对应的 “str_*” 和 “is_*” 函数。 因为 ctype 使用的是原生 C 库,所以会有明显的性能优势”。在4.2.0版本后,这些函数是默认启动的。

  • ctype_alpha()纯字符检测
  • ctype_upper()大写字母检测
  • ctype_lower()小写字母检测
  • ctype_digit()纯数字检测
  • ctype_alnum()检查字符串内的字符否全部为字母或数字
  • ctype_cntrl()控制字符检测
  • ctype_print()字符是否都可以打印
  • ctype_graph()字符输出是否都是可见的
  • ctype_punct()字符是否都可打印却不是字母数组和空白
  • ctype_space()空白字符检测
  • ctype_xdigit()十六进制字符串检测

正则表达式

在PHP5.3版本后,原来的POSIX Regex不再推荐使用。兼容Perl的正则表达式库PCRE仍可以使用,且默认开启。这里仅介绍PCRE相关函数,它们均以preg开头。

  • preg_match()返回pattern在subject中的匹配次数
  • preg_match_all()搜索subject中所有匹配pattern给定正则表达式的匹配结果并且将它们以指定顺序输出到matches结果中.
  • preg_replace()执行一个正则表达式的搜索和替换,当$pattern和$replacement都是数组时,会进行相对应位置的替换。
  • preg_grep(string $pattern , array $input [, int $flags = 0 ])返回给定数组input中与模式pattern 匹配的元素组成的数组.
  • preg_split()通过正则表达式分割字符串,返回一个数组。

JSON

自5.2.0起,JSON拓展默认内置并编译进PHP。

  • json_encode()JSON编码一个变量。
  • json_decode()解码一个JSON格式的字符串
  • json_last_error()返回JSON编码时最后的错误

多字节字符串

在汉语中,每个字符通常占用2个字节,在使用string的相关函数时,可能会出现意外问题。多字节字符串即为了解决此问题。这不是一个默认扩展,需要在configure选项中显式激活。详见安装。另外,mbstring支持“函数重载”,即使用mb_xxx替代原有的字符串函数。

  • mb_detect_encoding()检测字符的编码
  • mb_ereg_xxx打头的与preg_xxx同名的函数为正则匹配多字节版
  • mb_strlen()获取字符串长度。
  • mb_split()使用正则表达式分割多字节字符串
  • mb_substr()执行一个多字节安全的substr()操作
  • mb_strpos()查找字符串在另一个字符串中首次出现的位置;类似地,mb_strrpos查找最后出现的位置。
  • mb_strstr ()查找字符串在另一个字符串里的首次出现;类似地,mb_strrchr()查找指定字符在另一个字符串中最后一次的出现

BCMath

这部分函数进行任意大小和精度的数字的二进制计算。自4.0.4后随PHP一起发布。Windows版本下是默认支持的。

  • string bcadd(string $left_operand , string $right_operand [, int $scale ] )加法。scale用来决定小数点位数,输入输出均未string类型,下同。
  • bcsub()减法
  • int bccomp()比较
  • bcmul()乘法
  • bcdiv()除法
  • bcmod()取模
  • bcpow()乘方
  • bcsqrt()二次方根
  • bcscale()设置所有bc数学函数的默认小数点位数

图像处理

PHP可以处理各种格式的图像,并把它们输出到浏览器。这需要在编译时指定GD库(除了getimagesize()函数)。GD库不仅能处理图像,还能对字体进行处理。使用PHP可以动态修改图像文件,或为图像添加水印信息,甚至创建一个图像。

下面是和图像信息相关的函数:

  • gd_info()获取当前安装的GD库信息
  • getimagesize()获取图像大小,返回数组类型,按顺序分别是宽度,高度,类型,描述宽高的字符串。getimagesizefromstring函数则通过打开的图片信息(字符串格式)中读取图像尺寸信息
  • image_type_to_extension()获取图像类型的文件后缀
  • imageistruecolor(resource $image)检查图像是否为真彩色
  • imagesx()返回image所代表的图像宽度;imagesy()返回所代表的图像高度
  • imagetypes()返回PHP支持的图像类型,int类型。

剩下还有众多以image开头的和创建、输出、删除图像,画图、编辑图片、设置颜色、设置字体相关的函数,见参考

Exif

通过Exif拓展,可以操作图像元数据。必须使用--enable-exif选项编译PHP,Windows用户还需要启用mbstring扩展。

  • exif_imagetype()读取一个图像的第一个字节并检查其签名
  • exif_read_data()函数从JPEG或TIFF图像文件中读取EXIF头信息
  • exif_thumbnail()读取TIFF或JPEG图像中的嵌入缩略图。如果图像不包含缩略图则返回FALSE

Socket

Socket拓展基于流行的BSD sockets,实现了和socket通讯功能的底层接口。在编译PHP时必须在配置中添加—enable-sockets配置项。利用这部分函数可以很方便地搭建起socket服务器和客户端,示例见官网

  • socket_create(int $domain , int $type , int $protocol)创建并返回一个套接字(resource类型)。其中domain指定使用的网络协议族,type指定建立的套接字类型,protocol指定使用的具体协议。
  • socket_create_listen()在某端口打开socket以接收连接。
  • socket_bind()绑定网络地址到套接字的源。
  • socket_connect()使用address作为目的地址,建立套接字连接。
  • socket_listen()在创建好socket资源,并绑定了source address后,可以调用此函数监听进入的数据流。
  • socket_accept()在依次使用socket_create创建套接字,使用socket_bind绑定端口,使用socket_listen监听连接后。该函数允许到此套接字上的连接,返回一个新的socket资源用来通信。
  • socket_read()从已连接的socket中读取一段长度的数据,返回读出的数据。
  • socket_recv()功能同上,返回字节数并将数据存放在$buf中。
  • socket_recvfrom() 从已连接和还未连接的socket中读取数据。
  • socket_write()向socket中写入数据
  • socket_send()向已连接的socket中写入数据。
  • socket_sendto()向socket中发送数据而不管是否已连接
  • socket_getsockname()socket_getpeername()获取本地和远端socket信息
  • socket_set_block()socket_set_nonblock()设置socket是否阻塞
  • socket_set_option()设置套接字选项
  • socket_shutdown()停止从socket中读写数据
  • socket_close()关闭给定的socket资源

外部拓展库

这些扩展库已经绑定在PHP发行包中,但是要编译以下扩展库,需要外部的库文件。这里仅介绍常用的cURL库。Mysqli和Mongo等可能会用得到的库介绍从略。

client URL

PHP支持Daniel Stenberg创建的libcurl库,能够连接通讯各种服务器、使用各种协议。这些curl函数在PHP 4.0.2中引入。需要安装libcurl包才能使用PHP的cURL函数。安装过程从略。curl的使用流程思路和socket,mysql等十分相似,先使用curl_init()初始化会话,再使用curl_setopt()设置选项,然后通过curl_exec()执行会话,最后使用curl_close()关闭。

  • curl_setopt()设置一个传输选项,常用的设置包括CURLOPT_URL, CURLOPT_HEADER, CURLOPT_RETURNTRANSFER, CURLOPT_TIMEOUT等。类似的,还有curl_setopt_array函数。
  • curl_reset()重置一个libcurl会话句柄的所有的选项
  • curl_exec()执行一个cURL会话。返回TRUE或执行的结果,或是FALSE。
  • curl_close()关闭一个会话,释放所有相关资源
  • curl_getinfo()获取最后一次传输的相关信息。
  • curl_error()返回一条最近一次cURL操作明确的文本的错误信息
  • curl_version()获取cURL版本信息

特色

这里列举的特点更多是PHP语言的特殊使用方式与应用特性。如HTTP用户认证(介绍见官网),cookie等。

PHP透明地支持HTTP cookie,在PHP的网络函数中可以用setcookie()setrawcookie()函数来设置cookie。cookie是HTTP标头的一部分,因此setcookie()函数必须在其它信息被输出到浏览器前调用,这和对header()函数的限制类似。可以使用输出缓冲函数来延迟脚本的输出,直到按需要设置好了所有的cookie或者其它HTTP标头。

PHP允许用户使用POST方法上传文本和二进制文件。一个上传文件的HTML表单代码类似如下,其中的MAX_FILE_SIZE隐藏字段在浏览器端限制了文件大小(单位字节,不建议依赖于此):

1
2
3
4
5
6
7
8
<!-- The data encoding type, enctype, MUST be specified as below -->
<form enctype="multipart/form-data" action="__URL__" method="POST">
<!-- MAX_FILE_SIZE must precede the file input field -->
<input type="hidden" name="MAX_FILE_SIZE" value="30000" />
<!-- Name of input element determines name in $_FILES array -->
Send this file: <input name="userfile" type="file" />
<input type="submit" value="Send File" />
</form>

全局变量$_FILES自PHP4.1.0起存在,包含了所有上传的文件信息。$_FILES['userfile']数组有name, type, size, tmp_name, error等字段。error字段状态码在0-7间,分别表示上传成功/文件过大/部分上传/没有文件/找不到临时文件夹/写入失败。文件被上传后,默认地会被储存到服务端的默认临时目录中。

PHP支持同时上传多个文件并将它们的信息自动以数组的形式组织。要完成这项功能,需要在HTML表单中对文件上传域使用和多选框与复选框相同的数组式提交语法。像下面的代码那样:

1
2
3
4
5
6
<form action="file-upload.php" method="post" enctype="multipart/form-data">
Send these files:<br />
<input name="userfile[]" type="file" /><br />
<input name="userfile[]" type="file" /><br />
<input type="submit" value="Send files" />
</form>

同时,PHP还支持PUT方法上传文件,内容见官方文档。下面是一个允许用户上传图片的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
header('Content-Type: text/plain; charset=utf-8');
try {
// Undefined | Multiple Files | $_FILES Corruption Attack
// If this request falls under any of them, treat it invalid.
if (
!isset($_FILES['upfile']['error']) ||
is_array($_FILES['upfile']['error'])
) {
throw new RuntimeException('Invalid parameters.');
}

// Check $_FILES['upfile']['error'] value.
switch ($_FILES['upfile']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new RuntimeException('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException('Exceeded filesize limit.');
default:
throw new RuntimeException('Unknown errors.');
}

// You should also check filesize here.
if ($_FILES['upfile']['size'] > 1000000) {
throw new RuntimeException('Exceeded filesize limit.');
}

// DO NOT TRUST $_FILES['upfile']['mime'] VALUE !!
// Check MIME Type by yourself.
$finfo = new finfo(FILEINFO_MIME_TYPE);
if (false === $ext = array_search(
$finfo->file($_FILES['upfile']['tmp_name']),
array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
),
true
)) {
throw new RuntimeException('Invalid file format.');
}

// You should name it uniquely.
// DO NOT USE $_FILES['upfile']['name'] WITHOUT ANY VALIDATION !!
// On this example, obtain safe unique name from its binary data.
if (!move_uploaded_file(
$_FILES['upfile']['tmp_name'],
sprintf('./uploads/%s.%s',
sha1_file($_FILES['upfile']['tmp_name']),
$ext
)
)) {
throw new RuntimeException('Failed to move uploaded file.');
}
echo 'File is uploaded successfully.';
} catch (RuntimeException $e) {
echo $e->getMessage();
}

在php.ini文件中激活了allow_url_fopen选项后,可以在大多数需要用文件名作为参数的函数中使用HTTP和FTP的URL来代替文件名。同时,也可以在includeinclude_oncerequirerequire_once语句中使用URL。如果有合法的访问权限,以一个用户的身份和某FTP服务器建立了链接,还可以向该FTP服务器端的文件进行写操作。

持久的数据库连接是指在脚本结束运行时不关闭的连接。当收到一个持久连接的请求时。PHP将检查是否已经存在一个(前面已经开启的)相同的持久连接。如果存在,将直接使用这个连接;如果不存在,则建立一个新的连接。所谓“相同”的连接是指用相同的用户名和密码到相同主机的连接。

PHP 5.3后使用GC作为新的垃圾回收机制。每个php变量存在一个叫”zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是”is_ref”,是个bool值,用来标识这个变量是否是属于引用集合(reference set)。第二个额外字节是”refcount”,用以表示指向这个zval变量容器的变量(也称符号即symbol)个数。通常,PHP中的垃圾回收机制,仅仅在循环回收算法确实运行时会有时间消耗上的增加。但是在平常的(更小的)脚本中应根本就没有性能影响。

安全

PHP作为一种强大的语言,无论是以模块还是CGI的方式安装,它的解释器都可以在服务器上访问文件、运行命令以及创建网络连接等。这些功能也许会给服务器添加很多不安全因素,但是只要正确地安装和配置PHP,以及编写安全的代码,那么PHP相对于Perl和C来说,是能创建出更安全的CGI程序的。这部分提出一些原则,在不同环境下尽可能提高安全性。

安装

以CGI模式安装PHP时,它的设计可以用以避免访问系统文件和服务器的任意目录。在安装时配置一些选项可以有助避免这类攻击。具体见文档介绍。同理,以Apache模块安装时,权限的注意也请见官网介绍

Session安全

这部分见Session部分的安全介绍。

文件系统

PHP被设计为以用户级别来访问文件系统,所以完全有可能通过编写一段PHP代码来读取系统文件如/etc/passwd,更改网络连接以及发送大量打印任务等等。因此必须确保PHP代码读取和写入的是合适的文件。

由于PHP的文件系统操作是基于C语言的函数的,Null字符在C语言中用于标识字符串结束,一个完整的字符串是从其开头到遇见Null字符为止。因此,任何用于操作文件系统的字符串(特别是程序外部输入的字符串)都必须经过适当的检查。

这种安全问题也会出现在执行来自用户输入的命令。通常有两条路可以选择:1)检查所有来自外部的变量(黑名单);2)后台写死可以执行的文件名或命令有限集(白名单

数据库

由于敏感数据和机密数据通常存储在数据库中,数据库安全和保护显得尤为重要。PHP本身并不能保护数据库的安全。这里只是讲述怎样用PHP脚本对数据库进行基本的访问和操作。

设计数据库时,永远不要使用数据库所有者或超级用户帐号来连接数据库,因为这些帐号可以执行任意的操作。应该为程序的每个方面创建不同的数据库帐号,并赋予对数据库对象的极有限的权限。同时,一些功能可以用视图(view)、触发器(trigger)或者规则(rule)在数据库层面完成。

连接数据库时,把连接建立在 SSL 加密技术上可以增加客户端和服务器端通信的安全性,或者SSH也可以用于加密客户端和数据库之间的连接。

存储模型中,可以散列一些没必要明文显示的数据,建议加盐散列,同时采用新的SHA散列算法(如SHA-2或SHA-3)以增加安全程度。

SQL注入

这部分内容是极为常见的网络安全问题,通过构造特殊的SQL语句,获取数据库信息甚至主机权限,介绍从略。

在预防措施上,永远不要信任外部输入的任何数据,包括表单里和cookie的信息

  • 使用权限被严格限制的帐号访问数据库
  • 检查输入的数据是否具有所期望的数据格式
  • 减少SQL语句的拼接使用
  • 使用数据库特定的敏感字符转义函数
  • 还可以选择使用数据库的存储过程和预定义指针等特性来抽象数库访问,使用户不能直接访问数据表和视图

错误报告

错误报告是一把双刃剑。一方面可以提高安全性,另一方面又有利于攻击者收集服务器的信息以便寻找弱点。PHP的独有的错误提示风格可以说明系统在运行 PHP,一个函数错误可能暴露系统正在使用的数据库,一个文件系统或者PHP的错误就会暴露web服务器具有什么权限,以及文件在服务器上的组织结构等。

有三个常用的办法处理这些问题。第一个是彻底地检查所有函数,并尝试弥补大多数错误。第二个是对在线系统彻底关闭错误报告。第三个是使用 PHP 自定义的错误处理函数创建自己的错误处理机制

可以通过error_reporting()帮助找到错误所在并使代码更安全。发布程序前,设置为E_ALL找到所有使用不当的地方;正式发布后,设为0彻底关闭错误报告或设置php.ini中的display_errorsoff

隐藏PHP

一些简单的方法可以帮助隐藏 PHP,这样做可以提高攻击者发现系统弱点的难度。在php.ini文件里设置expose_php = off,可以减少他们能获得的有用信息。

另一个策略就是让web服务器用PHP解析不同扩展名。无论是通过.htaccess文件还是Apache的配置文件,都可以设置能误导攻击者的文件扩展名。

更多机智的隐藏方法见官网隐藏PHP一节。

内核

考虑到重点所在,这部分内容仅简单地介绍一些涉及到PHP内部原理的东西。由于PHP运行在C语言的基础上,以下的内容和C语言编程靠近。

内存管理

用C语言编程时,开发者要手工地进行内存管理。因为PHP经常用作Web服务器的模块,内存管理与预防内存泄漏紧密关联。此外,Zend引擎要面对一个十分特殊的使用模式:在一段比较短的时间内,许多zval结构大小的内存块和其他的小内存块被请求又再被释放。为了满足以上的需求,Zend引擎提供为了处理请求相关数据提供了一种特殊的内存管理器。请求相关数据是指只需要服务于单个请求,最迟会在请求结束时释放的数据。API介绍从略

因为安全原因,在请求结束时,Zend引擎会释放所有由上面提到的API所分配的内存。

变量使用

PHP变量,通常来说,由两部分组成:标签(例如,可能是符号表中的一个条目)和实际变量容器。变量容器,在代码中称为zval,掌握了所需处理变量的所有数据。 包括实际值、当前类型、统计指向此容器的标签的数量,和指示这些标签是引用还是副本的标志。在PHP 5.3中,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _zval_struct zval;

typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct { /* string type */
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;

struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};

有关函数,类和对象,流等的介绍从略。

FAQ

Q: PHP 版本之间有什么联系?
A: PHP/FI 2.0是最早的PHP版本,已经不再支持。PHP 3是PHP/FI 2.0的后继者,要好很多。PHP 5是目前一代的PHP,内部使用了Zend 2引擎,除了很多新功能之外还提供了许多附加的面向对象编程(OOP)特性。


Q: 可以同时运行几个不同版本的PHP吗?
A: 可以,请参阅见PHP源程序发行包中的 INSTALL文件。


Q: 应该上哪儿去找我的php.ini文件
A: UNIX中默认在/usr/local/lib目录中,也就是<install-path>/lib。可以在编译时通过 --with-config-file-path标记来改变路径。Windows中php.ini文件的默认路径在Windows目录下。如果使用的是Apache服务器,则会首先在Apache的安装目录中寻找php.ini


Q: PHP是否仅限于处理GET和POST请求方法?
A: 不是,PHP有可能处理任何请求方法,例如CONNECT。适当的回应状态可以用header()发送。


Q: 我忘了PHP函数的参数顺序,它们是随机的吗?
A: 通常情况下,数组函数的参数里,needle在前,haystack在后;字符串函数中,haystack在前,needle在后。


Q: PHP选项register_globals对我有什么影响?
A: 强烈不建议开启此选项,register_globals会自动生成变量。


Q: 我需要直接访问请求报头中的信息,怎么能办到?
A: 如果以Apache的模块方式运行PHP,那么函数getallheaders()可以做这件事。


Q: 如果不建议使用常用散列函数保护密码, 那么我应该如何对密码进行散列处理?
A: 当进行密码散列处理的时候,有两个必须考虑的因素: 计算量以及“盐”。 散列算法的计算量越大,暴力破解所需的时间就越长。PHP 5.5提供了一个原生密码散列API, 它提供一种安全的方式来完成密码散列和验证。 PHP 5.3.7及后续版本中都提供了一个纯PHP的兼容库。PHP 5.3及后续版本中,还可以使用crypt()函数,它支持多种散列算法。针对每种受支持的散列算法,PHP都提供了对应的原生实现。


Q: “盐”是什么?
A: 加解密领域中的“盐”是指在进行散列处理的过程中 加入的一些数据,用来避免从已计算的散列值表(被称作“彩虹表”中对比输出数据从而获取明文密码的风险。


Q: 我在使用<input type="image">标记,但是没有$foo.x$foo.y变量,它们哪去了?
A: 当提交表单时,可以用图片代替标准的提交按钮,用类似这样的标记
<input type="image" src="image.gif" name="foo" />
当用户点击了图片的任何部分,该表单会被发送到服务器并加上两个额外的变量:foo.xfoo.y。因为foo.xfoo.y在PHP中会成为非法的变量名,它们被自动转换成了foo_xfoo_y。也就是用下划线代替了点。


Q: PHP 5中还能用MySQL吗?好像找不到了。
A: MySQL依然被支持,唯一区别是PHP 5中默认为不激活。这意味着在PHP的configure一行中不包含有--with-mysql选项,因此必须在编译时手工加入。Windows用户可以编辑php.ini并激活php_mysql.dll


Q: 在函数定义中,参数旁边的&是什么意思?
A: 这表示该参数是引用传递,该函数会修改其值。鼓励使用的方法是在函数定义中指定哪些参数应该用引用传递。在函数调用时通过引用传递参数是不推荐的,因为它影响到了代码的整洁。

参考

roadmap最近使用typescript重构到了1.0.0版本,下面是一些记录

1.0.0项目由两部分组成:Electron程序web网页。两者不直接关联,通过配置文件roadmap.config.json解耦。

另外,Electron程序和web网页属于两套开发流程,互不干扰,在使用配置文件作为接口的基础上,可以独立开发和升级。

Electron程序

提供给用户程序,通过输入地图配置、gpx文件、定位图片物料,产出roadmap.config.json。整体使用Electron + webpack + TypeScript的脚手架。

Electron工作重点在gpx文件转换和图片压缩:

  • gpx文件使用parse-gpx库解析,产出JSON字符串
  • 图片压缩,保留EXIF信息的图片压缩,产出压缩后的图片

最后加上用户的输入,综合产出roadmap.config.json

gpx解析

见src/main/gpx2json.ts

使用parse-gpx库解析,将经纬度坐标换算到百度地图坐标,产出保留经纬度、海拔信息的结构体,储存为JSON文件,便于网页读取。

图片压缩

见src/renderer/image.ts

带有EXIF信息的图片通常体积很大,不适合直接放在网页,会严重拖慢网页加载速度。而经过调研,常见的图片压缩工具都不会保留图片EXIF信息,即使保留也不会保留我们需要的经纬度、海拔信息。

另外,满足要求的图片压缩工具(如Adobe PhotoShop)没法整合在整个流程中。因此需要自己实现。

思路是:

  • 读取原始图片中的EXIF信息
  • 借助canvas压缩图片体积、同时调整图片尺寸
  • 再度组合EXIF信息和压缩后的图片,得到保留完整EXIF信息的压缩图片

网页模板

使用html-loader加载已经产出好的output.html,读取为字符串,直接输出到指定目录即可。

网页模板的开发流程见portal一节。

产物

样例见src/test/portal

产物生成在桌面的roadmap-output文件夹,新生成的文件夹会覆盖老的。内容如下:

  • index.html 目标网页
  • roadmap.config.json 配置信息
  • data
    • xx.json gpx内容
    • images 图片信息

roadmap.config.json

用于解耦。包含了基础的配置信息

  • city 默认定位的中心城市
  • title 网页标题
  • gpxCount gpx路径数
  • imgTitles 图片标题,不需要和图片一一对应

web网页(portal)

web网页为了便于迭代,使用了和Electron程序独立的webpack工程。在config中有独立的webpack配置,有独立的webpack调试、打包命令。

工程位于src/portal,使用TypeScript。产物位于portal目录下,由Electron程序引用。

在portal工程的webpack配置中:

  • 使用MiniCssExtractPlugin抽出css为css资源文件,加载时的避免样式闪动
  • 使用HtmlWebpackInlineSourcePlugin将引用的css和js文件inline,使得Electron程序只需引用一个HTML文件即可
  • 需要配置html-loader的attr,避免web网页在加载时,里面的<img>标签的src属性被解析

gps轨迹

使用百度地图API绘制polyline实现,Electron程序生成的JSON中,已经提前转成百度地图坐标地址。

图床

图片存储于免费的路过图床,因为不支持自定义访问路径,因此需要将上传图床后的路径保存为图片的title,在网页加载时,通过压缩图的title找到图片在图床上的对应地址(这个地方的设计待优化,所以暂时未开放)。

图片的位置使用EXIF.js读取压缩图片的EXIF信息拿到,转换坐标后绘制在地图上。

async_hooks

async_hooks是nodejs在8.2.1后引入的特性,目前仍然是Experimental状态。它被用来追踪NodeJS中异步资源的生命周期。

在async_hooks特性加入之前,想要了解异步调用上下文或追踪异步调用逻辑是件比较困难的事情:

  • 最早在v0.11中有实现AsyncListener,但在v0.12时被移除
  • 在Node6和7时,有非官方的AsyncWrap实现,指定回调函数监听异步资源的创建、调用前、调用后时机

async_hooks友好地解决了异步资源创建、调用的追踪问题:

  • 异步资源代表一个关联了回调的对象,回调可能被调用1次或多次,比如net.createServer()里的connect事件或fs.open()AsyncHook不区分这些场景,统一视作异步资源
  • 每一个异步上下文都有一个关联的id,即asyncId。asyncId是从1开始递增的,同一个async上下文中的id相同(在未enable async hook时,promise执行不会被分配asyncId)。executionAsyncId()可以获取当前异步上下文的asyncId,triggerAsyncId()获取触发当前异步上下文的异步上下文。借助asynId和triggerAsyncId可以追踪异步的调用关系和链路。
  • async_hooks.createHook()函数可以注册异步资源生命周期中init/before/after/destroy/promiseResolve事件的监听函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const async_hooks = require('async_hooks')
// ID of the current execution context
const eid = async_hooks.executionAsyncId()
// ID of the handle responsible for triggering the callback of the
// current execution scope to call
const tid = async_hooks.triggerAsyncId()
const asyncHook = async_hooks.createHook({
// called during object construction
init: function (asyncId, type, triggerAsyncId, resource) { },
// called just before the resource's callback is called
before: function (asyncId) { },
// called just after the resource's callback has finished
after: function (asyncId) { },
// called when an AsyncWrap instance is destroyed
destroy: function (asyncId) { },
// called only for promise resources, when the `resolve`
// function passed to the `Promise` constructor is invoked
promiseResolve: function (asyncId) { }
})
// starts listening for async events
asyncHook.enable()
// stops listening for new async events
asyncHook.disable()

executionAsyncId和triggerAsyncId

调用executionAsyncIdtriggerAsyncId函数获取当前异步上下文的asyncId和triggerAsyncId。

executionAsyncId的返回值由运行时决定,triggerAsyncId可以返回当前上下文的触发原因上下文id。见下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const server = net.createServer((conn) => {
// Returns the ID of the server, not of the new connection, because the
// callback runs in the execution scope of the server's MakeCallback().
async_hooks.executionAsyncId();
// The resource that caused (or triggered) this callback to be called
// was that of the new connection. Thus the return value of triggerAsyncId()
// is the asyncId of "conn".
async_hooks.triggerAsyncId();
}).listen(port, () => {
// Returns the ID of a TickObject (i.e. process.nextTick()) because all
// callbacks passed to .listen() are wrapped in a nextTick().
async_hooks.executionAsyncId();
// Even though all callbacks passed to .listen() are wrapped in a nextTick()
// the callback itself exists because the call to the server's .listen()
// was made. So the return value would be the ID of the server.
async_hooks.triggerAsyncId();
});

createHook

更常用地,我们使用async_hooks.createHook创建异步资源的钩子,注册异步资源生命周期各阶段的回调函数,目前支持init/before/after/destroy/promiseResolve这几种。

注意:打印信息到控制台也是一个异步操作,console.log()会触发AsyncHooks的各个回调。因此AsyncHook回调内使用console.log()或类似异步日志打印,会造成无限递归。一种解决办法是使用fs.writeFileSyncprocess._rawDebug这种同步日志操作。

1
2
3
4
5
6
7
8
9
const fs = require('fs');
const util = require('util');

function debug(...args) {
// Use a function like this one when debugging inside an AsyncHooks callback
fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' });
// OR
process._rawDebug(`${util.format(...args)}\n`);
}

init(asyncId, type, triggerAsyncId, resource)

可能会触发异步事件的资源构造时调用。这不代表后面的before/after事件回调会在destroy回调触发,只是说有这个可能。举个例子:

1
2
3
require('net').createServer().listen(function() { this.close(); });
// OR
clearTimeout(setTimeout(() => {}, 10));

参数解释如下:

  • asyncId 异步资源id
  • type 异步资源类型,字符串枚举值,具体参见官方文档
  • triggerAsyncId 触发当前异步资源创建的异步上下文的asyncId
  • resource 被初始化的异步资源对象

triggerAsyncId表示的是资源创建的原因,async_hooks.executionAsyncId()表示的是资源创建的时机。如下面例子里体现的一样:

1
2
3
4
5
6
7
8
9
async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId();
fs.writeSync(
1, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
}
}).enable();

require('net').createServer((conn) => {}).listen(8080);

nc localhost 8080后,打印信息如下:

1
2
TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0

before(asyncId)

在异步操作初始化完成(如TCP服务器接收新连接)或资源准备完成(写数据到磁盘),准备执行回调时触发。入参asyncId即这个异步资源的ID。before事件可能会触发0~N次。

  • 0次,异步操作被撤销
  • > 1次,持久化的异步资源,如TCP服务器

after(asyncId)

回调执行完成后立即触发。当执行回调过程中有未捕获异常,会在触发“uncaughtException”事件后触发。

destroy(asyncId)

当asyncId对应的异步资源被销毁时调用。有些异步资源的销毁要依赖垃圾回收机制,所以当引用了传递到init函数的resource时,destory事件可能永远不会被触发,从而造成内存泄漏。

promiseResolve(asyncId)

当Promise构造器中的resolve函数被执行时,promiseResolve事件被触发。有些情况下,有些resolve函数是被隐式执行的,比如.then函数会返回一个新的Promise,这个时候也会被调用。

new Promise((resolve) => resolve(true)).then((a) => {});语句执行时,会顺序触发下列函数:

1
2
3
4
5
6
init for PROMISE with id 5, trigger id: 1
promise resolve 5 # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
before 6 # the then() callback is entered
promise resolve 6 # the then() callback resolves the promise by returning
after 6

AsyncHook实例定义好后,需要通过enable开启。可以使用disable关闭AsyncHook的回调执行。

下面是一个AsyncHook的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let indent = 0;
async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId();
const indentStr = ' '.repeat(indent);
fs.writeSync(
1,
`${indentStr}${type}(${asyncId}):` +
` trigger: ${triggerAsyncId} execution: ${eid}\n`);
},
before(asyncId) {
const indentStr = ' '.repeat(indent);
fs.writeFileSync('log.out', `${indentStr}before: ${asyncId}\n`, { flag: 'a' });
indent += 2;
},
after(asyncId) {
indent -= 2;
const indentStr = ' '.repeat(indent);
fs.writeFileSync('log.out', `${indentStr}after: ${asyncId}\n`, { flag: 'a' });
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent);
fs.writeFileSync('log.out', `${indentStr}destroy: ${asyncId}\n`, { flag: 'a' });
}
}).enable();

require('net').createServer(() => {}).listen(8080, () => {
// Let's wait 10ms before logging the server started.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId());
}, 10);
});

在启动服务器后,打印信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before: 6
Timeout(7): trigger: 6 execution: 6
after: 6
destroy: 6
before: 7
>>> 7
TickObject(8): trigger: 7 execution: 7
after: 7
before: 8
after: 8

异常处理

可以直接参考官方文档描述

可以用来干嘛

一个最为人知的使用场景是我们下面会提到的CLS(Continuation-local-storage)。cls-hooked库通过async_hooks建立了context对象和当前async执行上下文的关系,从而在整个执行链(execution chain)上维护一个统一的数据存储。

还有一个是结合Performance Timing API这样的性能监测工具诊断整个异步操作流程的性能。比如这篇文章所介绍的。

参考

CLS

Continuation-local storage(CLS)类似线程编程里的线程存储,不过基于nodeJS风格的链式回调函数调用。它得名于函数式编程中的Continuation-passing style,旨在链式函数调用过程中维护一个持久的数据。

在node V8之前,分别基于AsyncListener和AsyncWrap实现。在V8后,基于async_hook实现的库名为cls-hooked。但使用方法一致。

这里借用cls README里的一个例子。假设你写了一个获取用户信息的模块,将获取到的用户信息放在session中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// setup.js

var createNamespace = require('cls-hooked').createNamespace;
var session = createNamespace('my session');

var db = require('./lib/db.js');

function start(options, next) {
db.fetchUserById(options.id, function (error, user) {
if (error) return next(error);

session.set('user', user);

next();
});
}

之后,需要将用户信息转化为一个HTML文档,你在另外一个文件中定义了转换函数,并从session中取出你想要的用户信息。

1
2
3
4
5
6
7
8
9
10
11
// send_response.js

var getNamespace = require('cls-hooked').getNamespace;
var session = getNamespace('my session');

var render = require('./lib/render.js')

function finish(response) {
var user = session.get('user');
render({user: user}).pipe(response);
}

使用

cls的使用围绕namespace展开,你可以根据需要自由组织namespace,需要持久化的信息读写在namespace的context上进行。

  • cls.createNamespacecls.getNamespace 创建和获取一个namespace
  • cls.destroyNamespacecls.reset 删除一个namespace和重置所有namespace
  • ns.getns.set 在namespace的context上读取和设置持久化数据
  • ns.runns.runAndReturnns.runPromise 在给定context下执行函数
  • ns.bindns.bindEmitter 绑定context到给定函数或eventEmitter
  • context 维护持久化数据的plain object

更多API参考文档

实现原理

正如上面所说,“cls-hooked库通过async_hooks建立了context对象和当前async执行上下文的关系”。下面有张图通过例子描述了cls的工作过程:

CLS workflow

简单拆解一下:

  • 首先,我们有一个典型的web server和应用上的中间件,我们在整个应用的生命周期里创建一个cls的namespace。
  • 新请求到达中间件时,cls通过ns.run(别的方式也行)创建一个空的cls context,并入栈该context,设置为active context。
  • 由于cls内部注册了AsyncHook,在init阶段,在Map中关联对应active context到当前asyncId。从而有异步操作(如查数据库)时,此前入栈的context就和操作的asyncId对应上。此后get
    、set操作都会针对同一active context进行。
  • 异步操作完成后,after回调触发,active context变成undefined,同时出栈当前context。当destroy回调触发时,会将关联到asyncId的context从Map中移除。

在cls-hooked实现中,

  • ns.getns.setns.active相关联
  • ns.active通过ns.enterns.exit变更或者在init回调中从contextMap中改变。
  • ns.enterns.exitinit回调最终都经由ns.runxxxns.bindxxx得到初始的context
  • cls-hooked借助async_hook和ns.enterns.exit保证异步流程中context和异步上下文的正确对应关系

考虑到cls-hooked的js代码可读性,可维护性和工程角度上还有改善空间,基于上面的原理,做了ts的重构,源码见这里(待补充),供大家参考和学习cls-hooked。

追踪logId

醉翁之意不在酒

有了cls的帮助,我们就可以利用它帮我们持久化logId,避免“continuation-passing-context”。可以写一个中间件,为req、res包装context,同时为每次请求持久化logId。在后面的controller、services这些位置就可以拿到之前持久化的logId。

一个express风格的中间件类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const cls = require('cls-hooked')
const uuidv4 = require('uuid/v4')

const clsNamespace = cls.createNamespace('app')

const clsMiddleware = (req, res, next) => {
// req and res are event emitters. We want to access CLS context inside of their event callbacks
clsNamespace.bind(req);
clsNamespace.bind(res);

const logId = uuidv4();

clsNamespace.run(() => {
clsNamespace.set('logId', logId);

next();
})
}

// controller.js
const controller = (req, res, next) => {
const traceID = clsNamespace.get('logId');
}

在这个思路的基础上,有类似cls-rtracercls-proxify这样的库,提供针对express、koa、fastify等常见后端框架的中间件,只需简单指定配置,便可以在请求的生命周期里透传logId,免去“continuation-passing-context”的尴尬,对已有代码侵入性也很小。有需要透传logId,但并不想(或暂时不能)使用后端框架的场景下可以考虑使用这种方案。

参考

0%