兑水的心灵鸡汤

好久不逛简书!

记不住曾经是以什么方式登录过简书,在我把QQ,微信,手机号,微博都试了一遍过后,我终于登录上去了。随便翻了翻首页,第一篇文章就是《心灵鸡汤是怎么忽悠我们的?》。匆忙看完,尚在回味之中,突然发现自己曾经也是一个鸡汤高手,生产过数十篇鸡汤软文,然后着急翻看,发现果真如此!

总结下来,我熬的心灵鸡汤大概有这样的特点:写自己的故事,还算完美的结局,极高的迷惑性,不认真看或者不了解我的人根本意识不到他们已经喝了我的鸡汤。

丑小鸭都能变成白天鹅,所以一切都会变好的。可实际上,丑小鸭之所以能够成为白天鹅,并不仅仅是因为它努力,勤奋,而是他的父母本身就是白天鹅。

爱笑的女孩子运气都不会太差。可实际上,如果一个女孩运气一直不好,她怎么可能笑得出来?

刚刚看完自己三年前的一篇鸡汤文,讲述的是一个不起眼的男孩的成长历程。这个故事就像丑小鸭的故事那样,那个男孩通过不懈的努力最终成为佼佼者。当然那个男孩不是我自己,而我的真实情况却是,整个高中三年,我因上课期间逃到校外上网不下10次,每个星期天唯一的半天假期我大都在网吧度过;由于我对手机的痴狂,我甚至一个月会换好几个手机;篮球水平也没有那么好。而至于我最终为什么能够考上大学,这个我真的不知道,也许我真有过刻苦和努力的那段时光吧。

我开始揣摩我当时的心理,可是依然没有任何头绪。我猜,我写文章的时候从来没想过让别人看见,我在给自己炖鸡汤,然后自己大口大口的喝下去。当刚刚喝完的时候可能我很解气,顿时觉得神清气爽有精神,可一段时间过后,我又感到烦躁无比,因为我又要面对摆在眼前那么真切的问题,我最终从自己幻想的那个世界中爬出来,面对强烈的反差,消极和怠慢丛生,然后开始耽误,一天,一周,一年,一辈子…

我继续看文章,有几篇是在我在外校做交流生的时候写的。不知道能不能算鸡汤文吧,但至少还是有一股鸡汤味。然后我又发现了一个共性,所有的文章都是我在遇到问题踌躇满怀的时候写的,比如刚来到陌生校园而找不到上课教室过后,和室友闹了小矛盾过后,离别的时候。这又能说明什么问题呢?当我开始把心中所有的烦躁和愤怒写出来过后,我的确好了很多,因为在构思一篇文章的时候,我出奇的冷静。当文章写完,消极和愤怒开始消退,短时间内信心爆棚,此时鸡汤的功效正在起作用;一段时间过后,我又开始烦躁,负能量急速增长,我想,我似乎得了鸡汤依赖症。所以不断的熬鸡汤,不断的喝自己熬的鸡汤,已经成为了我的常态。

做菜也许是我为数不过get到的技能,所以文章的最后讲讲我认为的好的熬鸡汤的方式。

材料:4-6个月母鸡,腌制萝卜整只多只,当归若干,枸杞少量,鲜竹笋数只,姜蒜,料酒,食盐若干。
步骤:

  1. 在剔除多余的胸脯肉和鸡腿肉过后,切成小块洗净;
  2. 加入食盐,料酒搅拌均匀静置半小时;
  3. 使用60度热水清洗干净,放锅里炒干水汽备用(可适当加入油和盐);
  4. 加入适当清水,片状的姜,整只蒜,以及大块的腌制萝卜,大火煮开;
  5. 倒入鸡肉,枸杞,小火炖上1小时;
  6. 加入切好的竹笋,再小火炖上1小时,鸡汤就可以出锅了,如果觉得不够鲜,还可以加入适当鸡精调味。
  7. 热腾腾的鸡汤,享用吧!

曾经有人给我说过,做程序员写代码要开心,不然就不要写了。我想说,鸡汤要好好做,不然还不如不做。

共勉!

END

分享到 评论

Flex弹性布局

说起布局,我脑子里一下就想到了DIV+CSS布局,毕竟曾经被那么多写着DIV+CSS网页开发的书籍洗过脑,然后到现在还不怎么会用这种大众的布局方式。当然了,其实页面还有其它的一些布局方式,比如表格布局,框架布局这样已经逐渐被淘汰的布局方式,也有今天的主角–一颗冉冉上升的新星,弹性盒子布局。

Flex是Flexible Box的简称,我们这里把其翻译为弹性布局,至于为什么不叫“灵活的盒子布局”。额,这个问题也许会在读完本篇文章找到答案。好吧,正式开始。

容器

flex是display的一个属性,当然对于行内元素还有一个叫做flex-inline的属性,这里我们不多说,但是要注意的是,一旦对一个元素的display属性设为flex,那么它的子元素就不能使用“浮动(float)”这个神奇的属性,而这个元素将会一跃成为容器(container),而其的子元素将会成为项目。好吧,先从容器说起。

一旦把一个元素的display属性设置成为’flex’,这个元素就成为一个容器容器有几个比较重要的属性,学习和掌握这几个属性是学习弹性布局的关键。它们分别是:

  1. flex-direction: 决定子元素(项目)的排列方向。
  2. justify-content: 指定子元素在主轴上的对齐方式。
  3. flex-wrap: 指定多行显示以及显示形式。
  4. align-items: 决定项目在交叉轴上的对齐方式。
  5. align-content: 定义项目在多轴线上的对齐方式。

好吧,大概就是这几个了,我们注意到在上面解释的时候提到了主轴交叉轴,这里我先简单解释一下:对于这个概念,我们可以很简单的在一个容器上画一个十字坐标轴,如果我们设置flex-direction为row(行),那么横坐标就为主轴,纵坐标就是交叉轴,这里要注意坐标轴的指向,因为同样有一个属性为row-reverse,此时主轴的方向指向和设置为row的相反方向。

项目

作为容器的子元素,项目同样有几个重要的属性:

  1. order: 控制项目的排列,默认为0,值越小则越靠前。
  2. flex-grow: 用于定义Flex项目的放大比例,默认为0,即使存在剩余空间,也不放大。
  3. flex-shrink: 用于定义Flex项目的缩小比例,默认为1,即空间不足,Flex项目将等比缩小。
  4. align-self: 允许单个Flex项目有不同于其他Flex项目的对齐方式。

然后

我并不会把每一个属性具体来讲,因为这样的文章在网络上的确太多了。我正在学习RN,所以在这里就写一个界面,其中的布局将会采用弹性布局,中途我会对布局进行简单的分析,以加深理解。
当写到这里的时候我就开始布局这个简单的页面,然后到现在才布局好,时间用了那么长,感觉像是过了一年…好了,不多说了,最终的界面如下图所示。

其实我也不知道自己写了啥,还是勉为其难的就叫其“登录页面”吧,现在我来分析一下这个页面哪些元素是容器和项目(当然都是项目啦),又在什么地方使用了什么属性。

页面整体采用了弹性布局,所以从整体来看我们一定使用了flex-direction属性,并且其值是column,所以这里的主轴一定是Y轴并且是方向向上。并且我们能够观察到,页面上所有的元素都是居中的,我们于是想到了在交叉轴上的对齐方式是: align-items: ‘center’,而其主轴上的对齐方式则是默认的justify-content: ‘flex-start’。

接下来来看两个不明显的,页面上有两个输入框,每个输入框其实都是一个View组件包裹,那么在这个组件内部,我们仍然使用了弹性布局(竖轴为主轴),为了让输入框上下居中,这里必须让justify-content: ‘center’;紧接着最后一行有两个按钮,这两个按钮同样在一个View组件中,并且这个View也是弹性布局,并且一定要设置flex-direction: ‘row’才能让这两个按钮排列在一行。我们同样可以给每个按钮赋予不同的order值,让其进行排序。

总结一下:
我们是用弹性布局完成了一个基本页面的布局(虽然这真的很丑陋),在这个简单的布局中我们使用到的属性并不多,但是的确比使用css+div布局来得更快,尤其是垂直上的居中,css是比较难以实现的。这里我们一共有四个容器,分别是最外层容器,每一个输入框外层容器,按钮组外层容器,当然,这里面所有的元素都能称为项目,这里就不在多说。

尾巴

说了这么多,那么我对flex布局的态度到底是什么?一句话总结,学习它,了解它,使用它。弹性布局我在刚开始学习React的时候就有了解过,过了也快半年了吧,当时看着阮一峰老师的文章,感觉怎么都不明白,然后渐渐抛之脑后。直到学期开始,我开始学习RN,再一次接触到Flex布局,才想起来画一些时间去了解,然后试着使用,最后再让自己记忆下来。相比第一次我接触Flex布局,我做出了改变,而这种改变是在时间并不充裕的情况下,弥足珍贵的。希望自己能够加油,也希望和我有同样压力的同学加油!

分享到 评论

生成器

大家对于异步这个词想必都不陌生,看到异步我可能最先想到的就是使用回调,再者我会使用Promise,可是使用回调处理异步控制流是有缺陷的:第一,基于回调的异步方式实在不适合大脑对于任务的思考(这点我深有体会);第二,存在控制反转的问题。而Promise链提供了我们以顺序的方式处理和表达异步流。而这篇文章的主角生成器(generator)也是一种顺序和看似同步的处理异步的方式,并且它比Promise更加优秀。

有关函数

不知道大家有没有这样思考过,一个函数一旦执行,其是否能够中断,我也没有仔细想这个问题,并且当看到这个问题,我会经验判断函数在执行过程中不会中断。到底是否中断,我们先写一个例子:

1
2
3
4
5
6
7
8
9
10
var a = 1;
function foo(){
a++;
bar();
console.log(a);
}
function bar(){
a++;
}
foo(); // 3

运行上面这个例子,最终的结果是3,这就意味着,当函数执行完a++后,函数foo从表面上来看被中断了,然后执行了bar函数,最后执行权交给foo,函数返回已经被修改的变量a的值。虽然通过上面的小例子,并且因为JS单线程的特性,我们似乎能够肯定JS是抢占式的,但实际情况是JS并不是抢占式的,虽然函数bar的执行打断了函数foo的执行,但这其实是一种“关联”(参考原型继承)。

初识生成器

现在让我们来认识生成器表达式,
同样是上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
function *foo(){
a++;
yield;
console.log(a);
}
function bar(){
a++;
}
var iter = foo();
iter.next();
bar();
iter.next(); // 3

分析一下:
首先我们创建了一个生成器foo(注意*),该函数里面多了一个yield,有过python经验的大概知道这是干什么的。然后创建了一个函数bar;再来看执行,这里var iter = foo()并没有执行生成器foo,而是构建了一个迭代器,然后使用next方法启动了迭代器foo,并且在碰到yield停止执行,此时已经执行了x++,然后执行函数bar,执行完后,x经过两轮自增,此时x的值为3;最后我们调用next方法,从上一次中断处继续执行,并且没有碰到yield表达式,一直执行到函数结束,打印变量x的值为3。

现在我们来回答什么是生成器,其是一个特殊的函数,在函数声明的时候函数名前面包含一个”*“,并且能够多次启动和暂停。好了,我认为仅此而已。

一些问题

为了把生成器运用到异步流程控制中,我们还需要更深层次的了解生成器。

生成器仍然是一种函数

既然说生成器其本质上还是一种函数,所以其仍然具备函数最基本的特性,能够传递参数,也能返回值。我们不妨写一个例子测试一下:

1
2
3
4
5
6
7
8
9
function *test(a, b){
a++;
yield;
return a + b;
}
var it = test(1,2);
it.next();
var res = it.next();
console.log(res.value); // 4

其实生成器和普通函数的一个很大区别是在运行期间,它不会直接运行,而是创建了一个迭代器,然后每调用迭代器的next方法,便向下执行直到到碰到yield或者执行完成暂停。而我们注意到调用next方法返回的其实是一个对象,其包含一个value属性,如果生成器返回值的话,那么该属性的值就为生成器返回的值。总的来说,生成器的运行到目前为止完全是依托迭代器。

好了,继续。

继续来看例子:

1
2
3
4
5
6
7
8
9
10
function *foo(a, b){
a++;
yield a;
return a + b;
}
var it = foo(1,2);
var mid = it.next();
console.log(mid.value); // 2
var res = it.next();
console.log(res.next); // 4

这个例子想表达的就是对于生成器而言,yield表达式总会返回一个值,而我们可以通过迭代器next方法生成的对象来得到。

继续来看一个更复杂的例子:

1
2
3
4
5
6
7
8
9
function *foo(a){
var b = a + (yield 'ok return!');
return b;
}
var it = foo(1);
var res = it.next();
console.log(res.value); // ok return!
res = it.next(2);
console.log(res.value); // 3

这个例子向我们展示了生成器是如何双向传递消息的,yield表达式可以发出消息响应next(…)的调用,而next(…)可以向暂停的yield表达式发送值。

多个迭代器

一个生成器的多个实例可以并发执行,并且可以彼此交互。

1
2
3
4
5
6
7
8
9
10
function *gen(a){
var b = a + (yield)
return b
}
var it1 = gen(1);
var it2 = gen(2);
it1.next();
var res = it1.next(3).value;
it2.next();
console.log(it2.next(res).value); // 6

这个例子想说明的是一个生成器的多个实例可以并发执行,并且是互不干扰的。

分享到 评论

Prototype

JS为了模拟面向对象“类”的实现,为了模拟类的复制行为,可能会使用一种叫做“混入”的方法,当然,这种方法和我们今天要说的原型并没有多大的关系。使用mixin的方式来模拟“类”的实现不常见,当然为了模拟”类“所付出的代价也会让我们得不偿失,JS中不并存在”类”,而是存在一种叫做原型的东西,请容我细细说来。

Prototype

我们直接来讲文章的主角Prototype,其实JavaScript中每个对象都有一个叫做[[prototype]]的属性,这个属性就是对其他对象的一个引用。基本上所有的对象在初始化时[[prototype]]都会被赋予一个值,关于这个值是什么以及如何访问这个[[prototype]]属性,我会在后面提到。

还是先看一个例子:

1
2
3
4
5
var obj = {
a: 1
}
var obj2 = Object.create(obj)
console.log(obj2.a); //1

上面的代码我们创建了一个对象obj,其包含一个属性a,我使用Object.create(obj)创建了一个新的对象,并把该对象的[[prototype]]属性赋值为obj,最后我们打印obj2中并没有显式声明的变量a,令人惊奇的是,我们成功的访问到了变量a,并且该变量的值为[[prototype]]属性引用对象obj的属性a的值!

我想解释下为什么要对这段代码写这么详细的解释,因为对于大多数接触过JS的童鞋而言,原型已经是见怪不怪了,可是当初我学习JS的时候,脑子里完全没有原型的概念,直到有一天我慢慢开始懂得原型,那个时刻,我的心情就像现在写这段解释的时候这么激动!

看完上面这段代码和冗长的解释,即使不了解JS的童鞋也对原型有了一定的认识。在这里我想再说一下,[[prototype]]到底有什么用,其实很简单,当我们试图引用某个对象的时候,在底层其实调用的是一个GET方法,而这个方法首先会查找对象本身存在这个属性与否,如果不存在则通过[[prototype]]访问其原型对象,如果还是不存在的话,则访问原型的原型对象(别忘了原型对象也是普通对象),知道找到或者达到尽头(Object.prototype)。这个道理很简单,如果你使用for…in循环遍历一个数组的话,也许你得到的结果除了数组成员,还包含一些其它成员(不信你试试看),这些成员就来自原型对象,并且是可枚举的,而对于in关键字,也会查找原型链上面属性。

在说类的时候,也许更恰当的是给类打上一个引号,因为JS中根本就不存在”类”,JavaScript中只存在对象,我们不使用“类”创建对象,更多时候我们直接创建对象。可有些时候,我们使用new关键字来初始化一个对象,我们甚至在ES6后开始使用class,extend等属于类的关键字,这貌似和我前面说的矛盾了…
接着看一个例子:

1
2
3
4
5
6
function A(){

}
console.log(A.prototype);// {}
var a = new A();
console.log(typeof a); // object

我们创建了一个函数A,并且这个函数有一个属性prototype,如果没记错的话,这是本篇文章第一次访问原型,然后我们使用new初始化了一个对象,有传统面向对象语言基础的同学就知道,这简直像极了“类”!我再次强调,JS中不存在类,而且此new非彼new,这里函数A在new关键字的作用下,新建了一个空白对象,并让其prototype指向的对象赋值给新建对象a的[[prototype]]属性(关联),当然这里面还会做一些其它的工作,不过大体上就这样了,很简单吧!

在JavaScript中,并不存在类的复制,我们不能创建一个类的多个实例,只能创建过个对象,只不过通过new这种方式创建的对象,其内部的[[prototype]]属性关联到同一个对象,这里所说的关联是建立一个联系,并不存在复制。

构造函数

既然不存在类了,这构造函数听着也很别扭,我们暂且给它打个引号吧。上面我们在说“类”的时候,我们就用到了”构造函数”,函数A就是所谓的“构造函数”,其本质上就是一普通函数,是JS的一等公民,要说真要有什么区别,函数名首字母大写算吗?也许是吧。

再来写一个例子:

1
2
3
4
5
function B() {}
console.log(B.prototype);
console.log(B.prototype.constructor === B);
var b = new B();
console.log(b.constructor === B);

我感觉我放了一个大招,突然让自己迷惑起来,这里我要说明的是,B.prototype和对象b有一个叫做constructor的属性,并且默认指向函数B。这个属性的名字会让我们对JS的误解加深,四级没过的都知道,constructor翻译过来可叫做“构造器”啊,那么既然B.prototype.constructor指向了B,我们还有什么理由不说B不是“构造函数”?讲到这里,我很无奈…

其实呢,JS中根本不存在什么“构造函数”,其就是普普通通的函数,只不过一旦加上new关键字,这个函数调用再也不是普通的函数调用,我们把它叫做“构造函数调用”。

这里不想再说下去了,写个复杂点的例子先:

1
2
3
4
5
6
7
8
9
function Student(name, city){
this.name = name;
this.city = city;
}
Student.prototype.showInfo = function(){
console.log(`name: ${this.name}, from: ${this.city}`);
}
var stu = new Student('limoer', 'Chongqing');
stu.showInfo(); // name: limoer, from: Chongqing

这里有两个值得注意的地方,每个通过”构造函数调用”而生成的对象都存在两个属性name和city;我们给Student.prototype上添加了一个“方法”,这样所有的新建对象都关联了这个对象,可以引用这个“方法”,关于this的使用,这里就不在提了。

在说了这么多过后,我想把“构造函数”称为“关联函数”,因为所谓的“构造函数”其实并不存在,或者说是,我们并不知道一个函数在创建好后是否是“构造函数”,而如果我们把它叫做“关联函数”,因为它本质上做的工作包含了建立对象和其原型对象的关联,当然,这个叫法是不恰当的。

再来看看constructor属性,一般情况下,任何一个普通对象都存在一个constructor属性,其实这个属性并不是其本身就有,而是当引用该属性的时候,其可以在该对象的原型链中找到。现在我急切的想写一个例子来表明一个问题:

1
2
3
4
function C(){}
C.prototype = {}
var c = new C();
console.log(c.constructor === C); // false

我不啰嗦了直接看问题,这里对象c的constructor属性竟然指向的不是创建它的那个函数C,这也侧面印证了我上面说的话,通过构造函数调用创建的对象不直接持有属性constructor而是从其原型链中“继承”而来,所以当我们想写一段包含继承的代码时,如果还想用constructor属性,需要做必要的修正。

1
2
3
4
5
6
function Main(){}
function Sub(){
Main.call(this)
}
Sub.prototype = Object.create(Main);
Sub.prototype.constructor = Sub;

在结束“构造函数”讨论的时候,提醒一句,尽量不要使用constructor属性,要问原因?我想我已经不那么直观的在前面说出来了。

如何关联

我在上面提到把“构造函数”叫做“关联函数”,这虽然是不恰当的,但也不是一无是处,因为使用new关键字的“构造函数调用”,其在创建一个对象过后,也把该对象的[[prototype]]属性关联到该函数的prototype上。当然,如何关联不止这一种方法,这里介绍一种使用更为普遍的方法,Object.create(proto)。

还是例子为先吧:

1
2
3
4
5
6
7
var obj = {
info: function(){
console.log('info');
}
}
var obj1 = Object.create(obj)
obj1.info(); // info

这里我们使用字面量的直接形式创建了一个对象obj,该对象包含一个方法info,然后使用Object.create()创建了一个新的对象,并且该对象内部的[[prototype]]属性指向obj,概括点来说,该方法创建了一个对象,并把其关联到指定的对象。

Object.create()是ES5才引进的,在这里我实现一个polyfill代码作为本篇的结束:

1
2
3
4
5
6
7
if(!Object.create){
Object.create = function(obj){
function Foo(){}
Foo.prototype = obj;
return new Foo();
}
}

当然,这个版本的polyfill代码无法做到更复杂的功能,而Object.create第二个参数可以指定要添加到新建属性的属性名、值等。

尾巴

我们如果要访问一个并不存在的属性,在内部将会使用[[GET]]方法,并且查找该对象[[prototype]]所关联的对象,该关联实际上定义了一条“原型链”,在查找属性的时候会遍历整个“原型链”。

关联两个对象的最常用的两种方法是:(1)使用new关键字进行“构造函数调用”,创建一个新对象并进行关联;(2)使用Object.create(),创建新的对象,并时期和传入对象关联。

最后再次强调,JavaScript并不存在类,所有继承的实现完全是基于原型链,不存在复制。

注:到底前面所说的原型链的尽头到底在哪里呢?答案是Object.prototype,对于一般的原型链而言,其最终都指向了Object.prototype,这个对象包含了许多对象通用的方法,例如obj.toString()&obj.valueOf()等。

分享到 评论

this

JavaScript中的this关键字一直都是学习JS的一个难题,我们往往不能准确的理解this在上下文中的含义,往往导致一些问题的发生。对于this关键字,我们需要知道的是:this即不指向函数自身也不指向函数的词法作用域,this在函数调用时被绑定,也就是说,它指向什么完全取决于函数在哪里被调用。

绑定规则

在探讨具体的绑定规则时,我们先来看一个函数调用位置的例子:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log('call foo');
bar();
}
function bar() {
console.log('call bar');
baz();
}
function baz() {
console.log('call baz');
}
foo();

我们声明了三个函数,相互嵌套。现在来寻找各个函数的调用位置,foo在全局作用域中被调用,bar的调用位置则是

1
2
3
4
5
6
7
8
9
10
> 下面来看看几种一般性的绑定规则

#### 默认绑定
看一个例子:
```javascript
var name = 'limoer';
function showName(){
console.log(this.name);
}
showName(); //limoer

这里

1
2
3
4
5
6
7
8
9
10
11
12
13
#### 隐式绑定
隐式绑定的规则是调用位置是否有上下文对象,即是否被某个对象拥有或者包含。

来看一个例子:
```javascript
function showName(){
return this.name
}
var obj = {
name: 'limoer',
msg: showName
}
obj.msg(); // limoer

对于函数showName而言,其在obj对象中被引用,即函数此时引用有上下文的对象,此时使用隐式规则,this将被绑定到这个上下文对象,所以

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

两点注意:
(1)属性引用链只有或则说是最后以层才会影响调用位置,修改下上面的例子:
```javascript
function showName(){
return this.name
}
var obj = {
name: 'limoer',
msg: showName
}
var obj2 = {
name: 'lindo',
obj: obj
}
obj2.obj.msg(); //limoer

(2)this的绑定与函数的调用位置息息相关,一个常见的问题就是被隐式绑定的函数会丢失绑定对象,即应用了默认绑定规则。

1
2
3
4
5
6
7
8
9
10
var name = 'lindo';
function showName() {
console.log(this.name);
}
var obj = {
name: 'limoer',
msg: showName
}
var show = obj.msg;
show(); // lindo

函数show虽然函数showName的一个引用,但是由于其指向的是函数的本身,此时bar是一个不带任何修饰的函数调用,应用了默认绑定规则。

显式绑定

和隐式绑定相反,显式绑定是一种“强制性”手段,把this绑定到某个对象上,JavaScript提供了这样的函数,call()&apply(),这两个函数是如何使用的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。

1
2
3
4
5
6
7
var obj = {
msg: 'inner'
}
function showMsg(){
console.log(this.msg);
}
showMsg.call(obj)

call 和 apply方法的区别仅仅是参数上的问题,除了第一个参数外,apply可以使用数组来传入剩余参数,而call则是以多参数的形式写出来。除了call和apply以外,还有一个bind方法,它接收一个对象,返回绑定了该对象的这个函数,此种方法是硬绑定,也就意味着,绑定不可更改,这里不再多做介绍。

new 绑定

这是最后一种绑定方式,即“构造函数绑定”。会在将new关键字的时候在阐述此种绑定。

小结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后
就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由new调用?绑定到新创建的对象。
  2. 由call或者apply(或者bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。
    一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个DMZ对象,比如ø = Object.create(null),以保护全局对象。
    ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和ES6之前代码中的self = this机制一样。
分享到 评论

提升

我们都知道对于任何声明在某个作用域内的变量,都将属于某个作用域。但是对于JS而言,变量的声明与作用域还存在一种微妙的联系,这种联系我们叫做提升

首先来看一个例子:

1
2
3
4
5
console.log(name); // undefined
var name = 'limoer';
age = 2;
var age;
console.log(age); // 2

这个例子很好的体现了提升,输出name的时候,由于变量的声明在输出语句之后,想当然的使用RHS查找,查找失败抛出错误。但实际情况却输出了undefined,这是因为对于变量name,其声明被提升了,但是赋值语句却没提前,所以输出undefined。而对于age变量,赋值在输出前,我们理所应当的认为前面的赋值会被覆盖,但是程序却出乎意料的输出了2,这同样是因为变量age的声明提升到了首部,然后再进行赋值,最后输出了结果2。

上面的例子可以改写成这样:

1
2
3
4
5
6
var name;
console.log(name);
name = 'limoer';
var age;
age = 2;
console.log(age);

其实,对于提升的讨论就是“先有蛋还是先有鸡”的讨论,通过上面的分析,显然我们可以得出“先有蛋(声明),后有鸡(赋值)”的结论。

继续看一个例子:

1
2
3
4
5
6
7
8
9
10
foo(); // call foo
function foo(){
console.log('call foo');
console.log(a);
var a = 1;
}
bar(); // TypeError
var bar = function(){
console.log('call bar');
}

这个例子说明了三个问题:
(1)函数声明存在提升;
(2)函数表达式不存在提升;
(3)每个作用域都会存在提升。
这里我只想解释一下为什么上面运行bar() 抛出类型错误,由于变量声明存在提升,所以实际执行的是下面的代码:

1
2
3
var bar;
bar();
bar = function (){...}

由于bar的提升,调用bar()的时候并没有发生赋值操作,此时bar为undefined,所以对undefined进行函数调用会抛出一个TypeError而不是ReferenceError。

那么问题来了,既然变量和函数声明都存在提升,那么当这两个同时出现的时候,谁的优先级更高呢?

同样,写一个例子如下:

1
2
3
4
5
6
7
8
foo(); // call foo function
var foo;
function foo(){
console.log('call foo function');
}
foo = function (){
console.log('call foo expressions');
}

好了,我们得出的结论是:函数声明提升的优先级更高。

注意:这里尽管foo的声明在函数foo声明之前,但是由于存在这样的规则所以被当做重复声明被忽略了,但是如果是函数声明,照样可以覆盖前面的声明。

小结

ES6引入的const和let关键字的一大特点就是使用这两个关键字声明的标识符不存在提升,这就意味着无法引用未声明的标识符,从而可以避免由提升带来的一些列问题。

对于var a = 1;这样常见的变量声明方式,我们应该把它想象成两个步骤:首先是在预编译阶段对变量a进行声明,接下来是执行阶段,对a进行赋值,而提升则是将所有的声明“移动”到作用域顶部的过程。加油!

分享到 评论

JS作用域浅析

我们都知道,JS的作用域其实包含了一系列的气泡,这些气泡包含了标识符(函数和变量)的定义,而这些气泡相互嵌套并且整齐排列。而在JavaScript中,这种气泡指的是函数作用域块级作用域

函数作用域

函数作用域是JS中最基本也是最常见的一种作用域。所谓函数作用域,指的是在函数声明的过程中产生的一个“气泡”,这个“气泡”可以包含标识符。

来看一个例子:

1
2
3
4
5
6
7
8
9
function foo(){
var a = 1;
function bar(){
var b = 2;
}
}
console.log(a); // 失败
console.log(b); // 失败
bar(); //失败

在上面的这段代码中,函数foo所形成的作用域包含了标识符变量a以及函数bar,而函数bar所形成的作用域包含了变量b。当然,全局作用域中也包含标识符函数foo

当然,就像上面程序的运行结果一样,直接访问变量a,b,函数bar都将失败。因为在函数作用域中,其声明的变量和函数中能在其内部(包含嵌套的作用域)使用。

由于函数作用域的特性,将会带来很多优点,譬如函数作用域可以隐藏函数内部的实现(非常重要),也可以避免变量在声明过程中产生的冲突以及覆盖。

块级作用域

有Java学习经验的童鞋对块级作用域可谓是了解,然而在JS中,块级作用域可不是那么常见(至少是在ES6出现以前)。

在let和const关键字出现以前,如果想找到块作用域的影子,那么只有with和try…catch语句了。

with关键字是JS块作用域的一个典型,在该作用域的范围内声明的变量都只在with语句块中有效。

而块作用域的另一个应用则是在try…catch中,相信即使对JS不够了解的童鞋都知道异常处理,对于JS中的try…catch语句,在catch块中将会产生一个err对象,而这一个对象只能在catch块中才能使用。看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
function showName(){
console.log(name);
}
try {
showName()
} catch (e) {
console.log(e);
} finally {
console.log('end');
}
console.log(e);

上面的这个例子showName函数内部试图打印一个并不存在的变量name,这里将使用一个RHS查找,并且在失败后抛出一个引用错误,我们可以在catch捕获到这个错误对象,但是我们没法在全局作用域上使用这个错误对象。

好了,除了这两个使用块作用域的典型,在ES6标准中,还新增了let和const关键字来实现块作用域。这里简单介绍一下,对ES6感兴趣的的童鞋们可以点这里来进一步了解ES6。

let和const都是有别于var的另外两种声明方式,let用于声明变量,该变量将会被绑定在{…}中,也就是说使用let声明的变量具有块级作用域。使用let声明的变量不但具有块级作用域,同时变量也不会提升。而const则用于声明常量,同样具有块级作用域,并且也不存在提升。let可以很好的用于循环,防止变量对于环境的污染。

小结

函数是JS中最常见的作用域,声明在函数内部的变量和函数将会被很好的隐藏起来,这是一种良好的设计原则。而在ES6中,块级作用域再次被人们所日常使用。块作用域到底是不是函数作用域的替代方案,我认为到目前为止,不是!我们应该自己选择使用何种作用域,如何结合使用这两种作用域,来创造更加可读和健壮的程序。

分享到 评论

词法作用域

作用域模式有两种,一种是词法作用域,另一种是动态作用域,JavaScript采用的是词法作用域。

大部分的编译器会在编译阶段把程序进行词法化,也就是会对源代码中的字符进行解析,并且赋予词语语义。简单来讲,词法作用域就是词法阶段的作用域,词法作用域是由你在写代码时讲变量和块写在哪里决定的,当词法分析器在处理代码时会保持作用域不变。

考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
var name = 'limoer';
function showNameAPI(name){
var city = 'Chongqing';
function showCity(){
var cid = 'CN';
console.log(city + cid);
}
showCity();
console.log(name);
}
showNameAPI(name)

这个代码一共包含三个逐级嵌套的作用域,全局作用域中声明了变量name,全局函数showNameAPI,函数作用域中showNameAPI所创建的作用域,包含标识符city以及showCity,最后是showCity创建的作用域,包含了标识符cid。

作用域查找会在找到第一个匹配的标识符时停止。这里的查找是由内而外的,并且在多级嵌套的作用域内可以定义同名的标识符,但是会产生覆盖。因为作用域查找的规则就是找到第一个匹配的标识符后停止

无论函数在哪里被调用,也无论其是怎么被调用的,其词法作用域只与其被声明的位置有关。

欺骗词法

上面说到词法作用域是在是完全在书写代码是就已经决定,但是也可以通过下面的两种方式在运行时来改变词法作用域。

当然,不出意外的,这两种方式会是不那么讨人喜欢的eval()和with。

我们首先来回顾一下eval(),这个函数接收一个字符串作为参数,这个字符串好像是运行时写在这里的代码一样。这明显是一种词法欺骗,其假装是在书写期间就在那里,而在运行时修改词法作用域。但是引擎对此并不知情,所以其依旧照常按照词法作用域进行查找。
看一个例子:

1
2
3
4
5
6
var name = 'limoer';
function showName(str){
eval(str);
console.log(name);
}
showName("var name = 'lindo'") // lindo

eval(‘name=”lindo”‘)会被引擎误认为在书写时就在那里,由于执行了上面的语句,此时name的值已经被修改了,并且产生了覆盖,遮蔽了外部同名的变量name。

在默认的情况下,如果eval中所执行代码中存在一个或者多个申明,其就会对eval()所处的作用域进行修改。无论何情况,eval(..) 都可以在运行期修改书写期的词法作用域。

再来谈with关键字,我们都知道with关键字用于重复引用一个对象的多个属性的快捷方式,而不需要重复引用对象本身。
看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
name: 'limoer',
age: 20,
city: 'Chongqing'
}
console.log(obj.name)
console.log(obj.age)
console.log(obj.city)
// 重复
with(obj){
console.log(name);
console.log(age);
console.log(city);
}

上面的这段代码突出了with关键字优点,它可以简化我们的代码,但是我们这里谈的是with关键字的词法欺骗,看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function info(obj){
with(obj){
name = 'lindo'
}
}
var p1 = {
name: 'limoer'
}
var p2 = {
city: 'Jinan'
}
info(p1)
console.log(p1.name) // 'lindo'
info(p2)
console.log(p2.name) // undefined
console.log(name) //lindo!

上面的这个例子很好的展示了with关键字的词法欺骗,这里创建了两个对象p1和p2,并通过info函数执行with(obj){…},这里进行了简单的LHS查找,并将新值赋给name属性。但是请注意,这里p2对象并不存在name属性,也不会创建name属性,所以p2.name为undefined;这里很好理解,但是为什么神奇的是竟然多出了一个全局变量name呢!?

这里执行with(obj){…}的时候,执行的LHS查找,所以当查找不成功时自动隐式创建一个全局变量,如果这样考虑,那么出乎意料的name属性就不难理解了。

总结

JavaScript拥有的是词法作用域,所谓的词法作用域就是在进行词法分析时的作用域,也就是说,JS的作用域在代码一旦书写完成就能确定(靠书写位置来确定)。词法作用域的理解很简单,但是我们还是需要注意使用eval()和with语句带来的词法欺骗的原因。也许有人会说,在运行时修改词法作用域有利于实现复杂的功能,又利于扩展,何乐而不为呢?可我们在前面提到,在进行编译的时候,JS引擎会对代码进行优化,而这个优化则是根据代码的词法作用域,预先确定变量和函数的位置,才能在执行过程中快速找到标识符。而eval()和with的出现则有可能打破这样的格局,因为引擎在词法分析阶段并不能知道传入的代码到底是什么,会对词法作用域造成怎样的影响。所以,一切优化都是徒劳的,因为在运行时谁都不能确定此时此法作用域到底是怎么样的,所以JS引擎并不会进行优化,导致代码运行缓慢,性能并不好。所以,尽量不要使用它们。

分享到 评论

理解作用域

JS作用域的问题是老生常谈的问题了,我们都知道JS是不存在块级作用域的(ES6以前,try…catch是一个例外),可是仍然实际编程过程中由于对作用域的理解不够深刻,踩坑无数,苦不堪言,恰逢寒假,又到了充(wan)电(shuai)的时间,所以把学习的重点放在认识JS语言本身上面。参考了资料《你不知道的JavaScript》。

实例为先

首先,我们以var a = 1;这一个语句为例,看看这个过程JS到底干了什么。

毫无疑问JS是一门编译型的语言,但它的编译不是提前编译的,编译过程一般发生在语句执行前几个微秒。并且其和其它典型的编译语言一样,编译的步骤是相似的。首先编译器会把语句进行分词,分成单个的代码块,这些代码块被称为词法单元;接下来编译器会把词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这棵树被称为”抽象语法树AST”;最后一步,代码生成,该过程会把AST转换成可执行代码(机器指令)。

当然,JS的编译过程不会如此简单,其也会包含性能优化等。但这些所有的工作都在执行前几个微秒的时间内完成,并且立即执行它。

在正式的开始了解作用域之前,我们首先认识一下代码在执行时所需要的工具。

  1. 引擎:负责代码编译到执行过程
  2. 编译器:负责词法分析和可执行代码的生成等
  3. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

现在开始对var a = 1的执行进行分解:

首先编译器会把这段程序分成词法单元,并且构建AST,当遇到var a的时候,首选编译器会询问作用域,是否已经已经有一个同名的变量存在于这个作用域集合中,如果存在则忽略该语句,否则在该作用域申明一个变量,命名为a;接下来,编译器会为引擎生成可执行代码,用于处理a=1这个赋值操作,引擎会询问当前的作用域是否存在一个名字为a的变量,如果存在,则使用这个变量,否则继续操作。最终,如果找到变量a,则给其赋值为1,否则将抛出错误。

总结:在对变量赋值时会存在两个步骤:首先编译器会在作用域中生成一个变量;然后引擎在运行时查找该变量,如果存在就赋值,否则抛出错误。

作用域

1.嵌套作用域:作用域嵌套这个概念是很好理解的,当一个块嵌套在另一个块或函数中的时候,就会发生作用域的嵌套。当在当前作用域无法找到某个变量的时候,引擎就会在外层的嵌套作用域中寻找,知道抵达到最外层作用域或者找到了该变量为止。

考虑以下代码:

1
2
3
4
5
var a = 1;
function add(b){
console.log(a + b)
}
add(2) // 3

对变量a的查找是无法在add函数内部完成的,所以引擎会向上层作用域中查找,并且在外层作用域中找到。

LHS && RHS

引擎查找变量时常用的两种查找方式,这里需要注意的,如果执行右侧查找,无法找到该变量则会抛出引用错误。而进行左侧查找的时候,如果不成功,则会自动的隐式创建一个全局变量(都是在非严格模式下)

小结

简单来说,作用域就是一系列的规则,这套规则用于管理引擎如何在当前作用域以及嵌套的子域中根据标识(zhi)符名称进行变量的查找。

下一篇,将深入的学习词法作用域

分享到 评论

我们的世界

最近看了一部叫做《我们的世界》的韩国剧情片,讲的是这样一个故事:

主人公李善被人孤立,没人愿意和她组队玩集体游戏,她总是最后一个被无奈选中,也不出意料的第一个被淘汰,然后孤苦伶仃的站在一旁,不知道是不是要离开。她在学期末结识了转校生韩智雅,不知道这是不是她结交的第一个朋友,但的确度过了一个快乐的假期。智雅父母离异,却在李善家中看到了温诺的家,于是心生情愫,逐渐远离李善,结交了新的朋友。从好朋友到最熟悉的陌生人,李善就这样在新学期开始,再次被孤立。而韩智雅为了不被大家孤立,不但选择装作不认识李善,反而和班上同学一起排挤她,这让李善儿感到很苦恼。父母的不知情以及后面的一系列的事,让她们两个的关系越来越糟糕。韩智雅成为班上的学霸,而李善也成为了以宝拉为首的小团体利用攻击智雅的工具。最终两人走上了互相伤害的道路,智雅说善儿的父亲是酒鬼,而李善儿反击,把自己知道的智雅撒谎的真相公之于众。最后的最后,一直被孤立的李善还是继续被孤立,而韩智雅也成为那个被孤立的人。故事的最后,班上又开始玩丢球游戏,这次大家不想和两个人玩游戏,李善率先被淘汰,站在一边,韩智雅被诬蔑,李善主动站出来为她说话,避免了僵持不下的尴尬和无助。最后两人站在场边,不时偷望对方。我不禁有这般假设,假如没有选择离开,如今又会怎样?可惜小孩子的思想太过复杂,捉摸不透。电影戛然而止,关于她们两个最终和好的画面留给观众去想。

好了,故事讲完了。李善儿和韩智雅最终还是被孤立,成为了校园冷暴力的受害者。回到现实,我也像她们俩一样,成了受害者,并且顺利的长大了。

故事是这样的

过了这么多年,对小学和初中那段时光的记忆很微弱了。直到前些日子,小学群里面,有人发了一张毕业照照片,照片中的我站在人群的中央,探出一个脑袋,不注意真还发现不了这个瘦小的孩子。而到了快过年的时间,这个群就特别活跃,我却发现,我根本插不上一句话,好不容易找到一个机会发了一条消息,却被无视了,我开始回想,那段小学的日子里,我也经常这样被忽视,也许到现在,他们根本不记得了我了。

我开始回想上小学的那段时光。低年级的时候,我是班上再平常不过的,个子矮小,学习成绩一般,零花钱少,还特别闷。在我的印象中,那个时候我经常中午要去外婆家吃饭,来回的路上会有很多的孩子,但是没人愿意和我一起走,如果我走过去主动和他们说话他们就跑,或者打我。直到后来,我来回的路上终于有了一个伴,那是外婆村子里读小学的一个半傻子(我也不知道该怎么形容),我们一起在小河沟捉鱼,玩弹珠,可也因为这样,其他孩子更不愿与我玩。那时候的我,却没有如电影里两个女孩子那样琢磨不透的小心思,只好选择沉默。

好吧,最终我还是有了第二个(第一个是外婆村子里的傻子)好朋友,在我三年级的时候,我和安静的同桌成为了好朋友,我们很多时候会在放学时候一起回家(貌似还不顺路),开始的时候,我们都不怎么说话,可我发现一个人的字可以写得那么工整,一个人的画可以画得那么漂亮,我开始主动找她说话,后来我们两个人之间似乎有说不完的话,每到下课,我们才不会去和其他同学一样,逛零食店,打乒乓球,玩跳绳。可这段时间没过多久,我的第二个朋友一年后转校离开了。我又开始了被孤立的时光,知道逐渐的适应这样的生活。

这种情况直到小学六年级才得到转机,我的学习成绩也越来越好,我的一个亲戚成了我的班主任,他也很关照我。我虽然依然很闷,但是还是想逃离被孤立。为了融入坏孩子团体,我也开始变‘坏’,我开始在学校小卖店赊账,大部分都是请同学吃了零食;我开始和他们放学不回家跑到网吧去玩,这也是我最开始接触到网络的时候。可即便是这样,我仍然无法摆脱不了被孤立,我能够打很好的乒乓球但是没人愿意加我一个,我也能玩弹珠,但是又有人和我玩?

后来,大家都升入了初中,整个初中我都是在极力的融入这个环境,总在坏孩子和好学生的角色切换,这样的生活我过了三年。初中毕业后,我只身一人去了一个陌生的学校读书,也就和当初的坏孩子军团失去了联系,也许他们也不记得了,那个曾经总是在帮他们跑腿和揽责(背锅?)的我,这个坏孩子。

我的那段故事讲到这个就要画上一个句号了,因为我在高中结识了最要好的几个朋友,我再也是那个被孤立的对象。虽然在不熟识我的人眼里,我仍然很闷,就像刺猬一样,让人不得接近,可现实中的我,不是这样😂

那么,我到底想说什么

我相信在小时候的学生时代,被迫承受孤独的还有很多人。他们是多么渴望有个人成为他们的朋友,玩伴。可奈何无论怎么努力,却还是一再的被孤立。大概是因为差异,微小的差异让我们成为了受伤害的对象。也许是因为我矮小,抑或是我长得丑,家庭条件不好,甚至因为我成绩好…这些都成为了我们被排挤的理由。我们在任何时间段都会受到这样的伤害,成人学会了忍耐,而对于小孩子,他们很多就像李善和韩智雅一样,最终互相伤害。当我们还小时,因该是我们率真的时候,我们的小心思却伤害了我们。当然,这一切的一切,学校和家长并不知情,这样的伤害也许就会持续下去,伴我们成长。

小尾巴

我是先看完简介的时候再看电影的,整个过程我心情很失落,我看到了我的小时候,我并没有被无尽的孤立的所吞噬,我也没有反抗去伤害他人,反而我适应了这样的生活,逐渐的我习惯了过一个人的生活。直到现在,当独处的时候,我会回想,如果如曾经厌倦了无尽的孤立选择伤害反抗,抑或是我本身就是孤立别人的’坏孩子’,如今我又怎样。奈何时间没有倒带,人生不会从头再来。

分享到 评论