SegmentFault 前端思考最新的文章
2016-05-04T01:00:09+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Ajax拦截器的实现
https://segmentfault.com/a/1190000005054143
2016-05-04T01:00:09+08:00
2016-05-04T01:00:09+08:00
crossea
https://segmentfault.com/u/crossea
2
<p>个人觉得编程和下棋类似,卡位非常重要。一开始占据着有利的位置,当你在面对变化的时候就会非常灵活。编程中的卡位就是将一些关键的地方预留好,方便以后扩展。比如前端开发中的全局的事件代理,核心组件的基类设计,ajax拦截器等。</p>
<p>最近在做一个ajax加密的需求,就是所有ajax请求的参数都需要经过加密,防止他人盗用api。如果有ajax拦截器,这个需求实现会非常简单,只需要在拦截器里面实现加密即可。第一期,我们利用前端框架本身的ajax拦截器,完成了http加密,一切都很顺利。后续的第二期则遇个麻烦事情,我们需要对所有的前端工程(webvie、管理后台、活动...)进行加密,这些工程都是独立工程,而且使用的框架也不太一样。这就意味着我们第一期的加密代码很难被重用。</p>
<p>怎么办?我们想到了从更底层的XMLHttpRequest上去做文章,如果能在这层做掉,则和上层使用的框架无关了。即使跨越多个工程,也可以使用同一套加密方法。</p>
<p>经过调研window.XMLHttpRequest是可以被覆盖的。如果我们能包装一个和XMLHttpRequest一摸一样的构造器来覆盖原有的,则问题就解决了。经调研,几乎所有前端框架或库在做ajax特性检测时,会首先尝试使用XMLHttpRequest。那么在ie下,我们也可以将ActiveXObject包装成一个XMLHttpRequest。</p>
<p>该实现已经作为一个开源的小js库发布了。浏览器可以兼容到ie6+,若有类似的需求可以直接使用。代码总共不到200行,详情可以戳 <a href="https://link.segmentfault.com/?enc=Yc%2BOhVM6ApxXu7NHoPvz9A%3D%3D.jvH%2BRD%2FiIZKcxgzbExYytPJ8VZbGmGb2gvrg0%2Fp8Ad2rTUaT9vwQ45P9IsmaNs%2FL" rel="nofollow">https://github.com/vinnyguitar/xhr-overwrite</a>。欢迎一起讨论,改进。</p>
深入理解javascript函数
https://segmentfault.com/a/1190000004629413
2016-03-17T21:48:57+08:00
2016-03-17T21:48:57+08:00
crossea
https://segmentfault.com/u/crossea
3
<p>函数是javascript中最重要的内容,也是其相对其他语言来说在设计上比较有意思的地方。javascript许多高级特性也或多或少和函数相关。本文将以函数为中心,对函数的各个关键知识点做简要介绍。</p>
<h3>函数是对象</h3>
<p>理解函数是对象,是准确理解函数的第一步。下面的代码就创建了一个函数对象。</p>
<pre><code>var sum = new Function("num1", "num2", "return num1 + num2;");</code></pre>
<p>每个函数都是Function类型的实例。Function构造函数可以接受多个参数,最后一个参数是函数体,其他参数均为函数的形参。由于其书写的不优雅和两次解析导致的性能问题,这种方式不经常被采用,但是这种写法对于理解函数就是对象是非常有帮助的。一般地,我们都用字面的方式来创建函数。</p>
<pre><code>var sum = function(num1, num2){
return num1 + num2;
}
//或者
function sum(num1, num2){
return num1 + num2;
}
</code></pre>
<p>以上两种定义函数的方法分别叫做函数表达式和函数声明,两者的效果是等价的,区别在于解析器向执行环境加载数据时对两者的处理不一样。解析器会率先读取函数声明来创建函数对象,保证其在任何代码执行之前可用;对于函数表达式,则必须等到解析器执行到对应的代码行,函数对象才被创建。</p>
<p>在javascript中,函数对象和其他对象一样,均被视为一等公民。所以函数可以被引用、可以作为参数被传递或作为返回值返回,这使得函数的使用非常的灵活。</p>
<h3>函数的执行</h3>
<p>函数对象代表了一个过程,和大多数语言一样通过函数调用表达式可以调用这个过程。但是javascript的函数对象还提供了另外两种调用方式,call和apply方法。call和apply方法的第一个参数用于指定执行环境中this的绑定,后面的参数用于指定函数的实际参数。call和apply的唯一区别是实参的形式不一样,call是用逗号分割,apply则是以数组传递。例如:</p>
<pre><code>//函数调用表达式
sum(1, 2);
//call方法
sum.call(this, 1, 2);
//apply方法
sum.apply(this, [1, 2]);</code></pre>
<p>不管用哪种调用方式,最终都是通过函数对象的[[Call]]方法实际调用这个过程。[[Call]]方法是javascript引擎内部使用的一个方法,程序不能直接访问它。[[Call]]方法接受两个参数,第一个参数指定this的绑定值,第二个参数指定函数的参数列表。为了表达方便,后面我们将[[Call]]方法的第一个参数称作thisArg。函数对象的call方法和apply方法可以显示指定thisArg,函数表达式则是隐式指定这个参数的。例如:</p>
<pre><code>var foo = function(){
console.log(this);
};
var obj = {name:'object'};
foo();
obj.foo = foo;
obj.foo();</code></pre>
<p>代码在浏览器的执行结果如下:</p>
<pre><code>Window {top: Window, window: Window, location: Location...}
Object {name: "object", foo: function}</code></pre>
<p>从执行结果可以看出,obj.foo()这种调用方法,隐式将调用它的对象obj作为了thisArg。但是为什么foo()这种调用方式this的绑定值是window这个全局对象?难道foo()这种调用方式将全局对象默认指定为thisArg?其实不是这样的。thisArg并不是和this关键字的绑定一一对应的,其中有一个转换过程。如下:<br>1.如果thisArg为undefined或者null,则this的绑定为全局对象。<br>2.如果thisArg不是Object类型,则将thisArg强制转型为Object类型并绑定到this。<br>3.否则this的绑定就为thisArg。<br>其实foo()这种调用方式thisArg的值为undefined,通过以上的转换过程将this绑定为全局对象。</p>
<h3>执行环境与闭包</h3>
<p>前面提到过执行环境(Execution Context)这个概念,简单来说执行环境就是函数在执行时所依赖的一个数据环境,它决定了函数的行为。程序执行流每次进入函数代码时都会创建一个新的执行环境。活动的执行环境在逻辑上形成了一个栈的结构。当函数执行完毕,其执行环境从栈中弹出并销毁。</p>
<p>每个执行环境都包含一个重要的组件:词法环境(Lexical Environment)。词法环境定义了javascript程序标识符到变量或函数的关联关系。词法环境包含了环境记录(Environment Record)和一个到外层词法环境的引用(如果有的话,否则为null)。环境记录记录了当前作用域下的变量或函数的绑定情况。有两种类型的环境记录,声明式环境记录(Declarative Environment Records)和对象环境记录(Object Environment Records)。声明式环境记录包含了当前作用域下标识符到变量声明和函数声明的绑定。对象环境记录是一个和特定对象绑定的环境记录,用于临时改变标识符的解析情况,比如在with子句中。</p>
<p>函数对象都有一个[[Scope]]属性,函数对象在创建时会将当前执行环境的词法环境的值赋予给[[Scope]]属性。这个属性是引擎的内部属性,程序无法访问到它。当程序流进入到函数时,javascript引擎会创建新的执行环境,同时也创建对应的词法环境。引擎会将当前作用域声明的变量和函数绑定到词法环境,同时将[[Scope]]属性的引用也添加到词法环境。程序在进行标识符解析的时候,会优先从当前的词法环境中搜索,搜索失败则向外层词法环境搜索,如果到最外层的全局环境还没搜索到则会抛出异常。</p>
<p>嵌套定义的函数会形成javascript中一个有趣的特性:闭包。闭包的形成是由于内层函数引用了外层函数在创建它时的词法环境。即使外层函数已经返回,执行环境已经销毁,但是内层函数依然能够通过词法环境的引用访问外层函数中定义的变量或函数。</p>
<h3>with和catch子句</h3>
<p>with子句和catch子句都能临时改变当前的词法环境。他们的方式是有些区别的。先看with子句。</p>
<pre><code>function foo(){
var background = '#ccc';
with(document){
body.style.background = background;
}
}</code></pre>
<p>当执行流进入foo时,这时会创建一个声明式词法环境。执行流进入with子句的时候,引擎会创建一个对象环境记录。此时with子句中的标识符解析都会先从document这个对象中查找。当with子句执行完之后,对象环境记录销毁。</p>
<pre><code>try{
//do something
}catch(e){
//handel error
}</code></pre>
<p>catch子句也能临时改变当前的词法环境。和with子句不一样的是,它会创建一个声明式词法环境,将catch子句中的参数绑定到这个词法环境。</p>
<h3>构造器与原型继承</h3>
<p>函数对象还有个非常重要的内部方法[[Construct]],当我们将new操作符应用到函数对象时就调用了[[Construct]]方法。此时的函数充当构造器的角色。下面的代码就通过[[Construct]]创建了一个对象。</p>
<pre><code>var Dog = function(){
}
var dog = new Dog();</code></pre>
<p>[[Construct]]方法的执行过程如下。<br>1.创建一个空对象obj。<br>2.设置obj的内部属性[[Class]]为Object。<br>3.设置obj的内部属性[[Extensible]]为true。<br>4.设置obj的[[Prototype]]属性:如果函数对象prototype的值为对象则直接赋给obj,否则赋予Object的prototype值。<br>5.调用函数对象的[[Call]]方法并将结果赋给result。<br>6.如果result为对象则返回result,否则返回obj。</p>
<p>每个javascript对象都有一个[[Prototype]]的内部属性,[[Prototype]]的值为一个对象,叫做原型对象。当程序在访问javascript对象的某个属性时,首先会在当前对象中搜索,搜索失败则到原型链中搜索,直到搜索到相应值,否则就为undefined。javascript的这种特性叫做原型继承。[[Construct]]方法的第四步是实现原型继承的关键,它指定了javascript对象的[[Prototype]]属性。</p>
<pre><code>var Dog = function(){
}
var animal = {};
Dog.prototype = animal;
var dog = new Dog();</code></pre>
<p>上面代码创建出来的dog对象的原型就为animal,它“继承”了animal对象的属性。原型继承是另外一种面向对象的模型,相对于“类”的继承模型来说,原型继承更加符合我们的现实世界的模型。原型继承在javascript也是有非常广的用途。</p>
<h2>结语</h2>
<p>函数这条线将javascript许多核心内容串起来了,个人觉得这也是javascript最有意思的地方。本文主要是根据Ecma-262第五版规范中相关内容进行的总结和整理,由于能力有限,如有理解上的错误,望批评指出。</p>
通过iframe实现页面的局部刷新
https://segmentfault.com/a/1190000004629322
2016-03-17T21:35:53+08:00
2016-03-17T21:35:53+08:00
crossea
https://segmentfault.com/u/crossea
4
<p>一个网站是由许许多多页面组成的,超链接将这些页面连接起来,给了用户一个完整的使用体验。用户通过超链接打开一个页面时,浏览器会回收当前页面然后再渲染新页面。有时候我们并不希望是这样,比如对于一个音乐网站,我们不希望用户在切换页面时中断当前歌曲的播放。这就需要做到页面的刷新是局部而不是全部。目前大概有两种方法可以实现这种功能。一是将整个网站做成只有一个页面,模块切换完全靠客户端的js实现;二是通过嵌套iframe的方式实现。iframe方式虽然传统,但是和单页面应用相比也有许多优势。比如:内存一般不会泄漏,seo很方便等。本文将主要介绍iframe的实现方式。</p>
<p><img src="/img/bVtAsm" alt="QQ20141214-1.png" title="QQ20141214-1.png"></p>
<p>如上图,在最外层的页面嵌套一个iframe,通过改变iframe的src值来实现页面的切换。我们可以将站内所有链接的target值设为iframe。由于iframe是一个独立的浏览器窗口,其切换并不会刷新其外层的容器,这样就实现了页面局部刷新的效果。效果是达到了,不过还存在一个问题,那就是页面的URL不能体现在浏览器的地址栏里,这样的用户体验是非常差的。浏览器的地址栏表示的是最外层页面的地址,如果改变这个地址就会造成整个页面的刷新。不过好在浏览器为URL增加了hash部分。hash并不是http协议的内容,只是在客户端使用的,hash的改变不会刷新页面。所以在外层,可以用hash来体现iframe页面的地址。比如iframe的地址为<a href="https://link.segmentfault.com/?enc=nPLUCkNO1DvbSN2DYUxNiQ%3D%3D.sVYiuR4yKw23wIneYW1xxOrOyElzV%2BdgetLgfDW8Q9U%3D" rel="nofollow">http://a.b.com/user?id=123</a> ,那么转换到浏览器地址栏可能是<a href="https://link.segmentfault.com/?enc=Ky7XllwLglLQnqyUtXyqfA%3D%3D.xc6maVTFxlTF1ASBimtfhPmGYDqi4ApFOwICFC%2Bz%2Fsk%3D" rel="nofollow">http://a.b.com/#/user?id=123</a>。 通过简单的改动就能实现此功能。</p>
<p><img src="/img/bVtAsy" alt="QQ20141214-12.png" title="QQ20141214-12.png"></p>
<p>上面的流程图反映了一次页面跳转的过程。在iframe中,需要实现一个全局的点击事件代理,将所有链接的点击事件拦截下来。拦截后,将链接href值转换成hash地址,并修改外层src值。外层src值改变后,会触发hashchange事件,在该事件处理函数中将hash地址再次转换成页面真实地址并刷新iframe。这里有个细节需要注意,刷新iframe时,不能直接设置src值或者location.href的值,因为这会使得iframe也相应产生一条历史记录,这回导致浏览器的前进后退需要点两次才生效。通过调用location.replace()方法可以避免iframe产生历史记录。不过该方法在ie10+浏览器下有个严重的bug。使用浏览器的前进和后退键时会导致所有页面的刷新,这就破坏了局部刷新的需求了。这应该是浏览器实现上的一个bug,没有办法直接解决,不过可以用其他方法绕过去。避免iframe产生历史记录的方法还有一种,就是在刷新iframe时,先要将原来的iframe节点删除,然后创建一个新的iframe节点,并将其src设为对应地址。此时iframe的刷新就不会产生多余的历史记录了。</p>
<p>可以看到内嵌iframe的方式实现是比较简单的,关键的一点就是要把浏览器的历史记录问题处理好。希望本文提供的信息能有所帮助。</p>
Angular使用总结
https://segmentfault.com/a/1190000004606794
2016-03-15T09:03:18+08:00
2016-03-15T09:03:18+08:00
crossea
https://segmentfault.com/u/crossea
0
<p>前端开发大部分时候面对两类问题:一是业务逻辑,二是界面展示逻辑。在一个管理系统或互联网产品中,我们首先会通过业务数据对整个业务系统建模。所谓业务逻辑,反映到程序层面就是对一些业务数据的增删改查操作;同时我们也需要将这些业务逻辑的操作结果反馈给用户,这就是界面展示逻辑。比如当你在发布一条微博,在业务逻辑上是多了一条微博数据,同时在界面上也需要将这条数据呈现出来。在我看来,用户界面本质上就是业务数据的一种对用户友好的展示形态。一个理想的前端架构是将业务逻辑和界面展示逻辑完全分离,两者独立变化,互不影响。在我们考虑业务逻辑的时候,就不要考虑界面展示的问题;在考虑界面展示问题的时候,就不要考虑业务逻辑的问题。如果将两者耦合在一起,那么在面对复杂问题的时候可能就会左右为难,无从下手。</p>
<p>MVC模式的主要目的就是将业务逻辑和界面展示分离。虽然对于C(Controller)是有争议的,但是M(model)和V(view)是普遍认同的。将业务逻辑和界面展示分离的关键也在model和view的分离。angular是一个非常优秀的MV*框架,非常好的体现了MVC的思想,本文将从该角度来介绍angular的一些使用心得。</p>
<h3>需求</h3>
<p><img src="/img/bVtuAb" alt="QQ20150102-1.png" title="QQ20150102-1.png"></p>
<p>这是一个后来管理页面,用来设置各个客户端的banner显示。页面主要有以下功能:<br>1.编辑banner信息,可以设置banner的类型和上传banner图片。<br>2.删除和插入。<br>3.上下移动banner顺序。<br>4.保存。</p>
<h3>实现业务逻辑</h3>
<p>不考虑页面展示,我们可以先实现业务逻辑。各个端banner列表的逻辑是一样的,只是类型以及与后台的交互接口不一样。我们可以将banner的这些列表逻辑定义成一个service,不同端的列表都可以重用这个service。其定义如下。</p>
<p><img src="/img/bVtuAq" alt="QQ20150102-2.png" title="QQ20150102-2.png"></p>
<p>这个service的实现非常简单,就是对一个banner数组进行增删改查。</p>
<h3>实现界面展示逻辑</h3>
<p>实现完业务逻辑我们再来解决界面展示的问题。angular提供了一个非常强大的模版引擎,通过指令和表达式就能将数据呈现到界面。如何才能模版中使用这些业务数据和方法呢?我们需要将这些数据和方法写到scope对象中,scope对象定义了对应模版能够使用的数据和方法。bannerCollection service的attach(scope)方法就是将对应的banner数据和方法写到scope对象中以便能在模版中使用。下面的代码是在mobile controller中使用bannerCollection service,并通过其attach方法和对应的scope对象绑定。</p>
<p><img src="/img/bVtuAx" alt="QQ20150103-2.png" title="QQ20150103-2.png"></p>
<p>将数据和方法注册到scope对象后,我们就可以在模版中使用了。下图是模版的一部分。</p>
<p><img src="/img/bVtuAD" alt="2105-1-3-1.png" title="2105-1-3-1.png"></p>
<p>ng-repeat指令用来展示banner列表,双花括号用来输出数据,ng-click用来响应用点击。angular框架提供了许多非常有用的内建指令,基本上能满足我们大部分的需求了。除了内建指令,angular还为我们提供了自定义指令的接口,通过自定义指令我们可以扩展出非常丰富的web组件。在这个banner管理页面中,有个上传banner图片的功能,我们就可以将其封装成一个指令。</p>
<p><img src="/img/bVtuAG" alt="direc2.png" title="direc2.png"></p>
<p>如上图,该指令的实现主要在link方法中。每个自定义指令都可以实现这个方法,当模版引擎在链接模版的时候会回调指令的link方法,调用时将当前的scope和element作为参数传进来。link方法非常重要,它体现了指令的本质:scope到element之间的一个桥梁,其实也就是model到view的一个桥梁。我们可以在这个link方法中监听element的事件来响应用户操作并修改scope中的数据;也可以监听scope中数据的变化将其反映到用户界面上。scope对象有个$watch方法,可以通过该方法去监听scope中需要关心的数据的变化。<br>在这个图片上传的指令中,我们在link方法中监听了用户选择文件的事件。当用户选择文件后,通过post请求将图片上传到后台生成url和dsfid,同时更新scope中的img数据;ng-repeat指令在监听到scope中img数据的变化后又会刷新相应的界面展示。</p>
<h3>结语</h3>
<p>angular为我们提供了一个非常好的业务逻辑和界面展示逻辑分离的机制,极大地简化了前端开发。特别对于数据型应用,angular是非常好的选择。</p>
从Ecma规范深入理解this
https://segmentfault.com/a/1190000003906484
2015-10-26T00:31:15+08:00
2015-10-26T00:31:15+08:00
crossea
https://segmentfault.com/u/crossea
11
<p>this是面向对象编程中的一个概念,它一般指向当前方法调用所在的对象,这一点在java、c++这类比较严格的面向对象编程语言里是非常明确的。但是在javascript中,this的定义要灵活许多,如果未准确掌握,非常容易混淆。本人在面试过程中也发现,面试者很少有由能够回答得非常全面的。本文总结了this的各种情况,并从Ecma规范的角度探讨了this的具体实现,希望对大家理解this有所帮助。</p>
<h3>this指向的四中情况</h3>
<p>在javascript里面,this的指向可以归纳为以下四种情况。只要能牢记这四种情况,大部分情况下就已经够用了。</p>
<p>1.在全局代码或者普通的函数调用中,this指向全局对象,在浏览器里面既为window对象。</p>
<pre><code> console.log(this);//输出window
function foo(){
console.log(this);
}
foo();//输出window</code></pre>
<p>在浏览器环境里运行上述代码,两处输出结果均为window对象。</p>
<p>2.通过call或apply方法调用函数,this指向方法调用的第一个参数。</p>
<pre><code> var obj = {name:'test'};
function foo(){
console.log(this);
}
foo.call(obj);//输出obj
foo.apply(obj);//输出obj</code></pre>
<p>在浏览器环境里执行以上代码,输出结果均为对象obj。call和apply除了参数形式不一样外其他都一样,call采用逗号分割,apply采用数组。说到这里,顺便介绍一个小技巧。如何在不生成新数组的情况下实现两个数组的连接?请看下面的代码。</p>
<pre><code> var arr1 = [1, 2 , 3],
arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1);//输出[1, 2, 3, 4, 5, 6]</code></pre>
<p>执行上述代码后,输出结果为[1, 2, 3, 4, 5, 6]。这是一个非常实用的小技巧,由于apply第二个参数为数组形式,所以我们可以把push方法“借”过来,从而实现两个数组的连接。</p>
<p>3.调用对象的方法,this指向该对象。</p>
<pre><code> var obj = {name:'test'};
function foo(){
console.log(this);
}
obj.foo = foo;
obj.foo();//输出obj</code></pre>
<p>执行以上代码后,控制台输出为obj对象。这就是我们常说的“谁调用,指向谁”。</p>
<p>4.构造方法中的this,指向新构造的对象。</p>
<pre><code> function C(){
this.name = 'test';
this.age = 18;
console.log(this);
}
var c = new C();//输出 c
console.log(c);//输出 c</code></pre>
<p>执行以上代码后,控制台输出均为c所指向的对象。当new操作符用于函数时,会创建一个新对象,并用this指向它。</p>
<h3>Ecma规范</h3>
<p>Ecma规范里面详细介绍了this的实现细节,通过阅读规范,我们可以更准确的理解上述四种情况到底是怎么回事。<br>函数对象有一个叫[[Call]]内部方法,函数的执行其实是通过[[Call]]方法来执行的。[[Call]]方法接收两个参数thisArg和argumentList,thisArg和this的指向有直接关系,argumentList为函数的实参列表。thisArg又是怎么来的呢?我们可以和前面讨论的四种情况对应起来:</p>
<ol>
<li><p>普通方法调用thisArg为undefined。</p></li>
<li><p>通过call或apply调用,thisArg既为第一个参数。</p></li>
<li><p>通过对象调用,thisArg指向该对象。</p></li>
<li><p>在构造方法中,thisArg为新构造的对象。</p></li>
</ol>
<p>thisArg和this是什么关系?规范里的描述是这样的:</p>
<ol>
<li><p>If the function code is strict code, set the ThisBinding to thisArg.</p></li>
<li><p>Else if thisArg is null or undefined, set the ThisBinding to the global object.</p></li>
<li><p>Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).</p></li>
<li><p>Else set the ThisBinding to thisArg.</p></li>
</ol>
<p>在严格模式下,thisArg和this是一一对应的。</p>
<pre><code>function foo(){
'use strict';
console.log(this);
}
foo();//输出undefined
</code></pre>
<p>该示例输出的结果为undefined。</p>
<p>第二点是说如果thisArg为null或者undefined则this指向全局对象。</p>
<pre><code>function foo(){
console.log(this);
}
foo.call(null);//输出window
</code></pre>
<p>该示例的输出结果为window。</p>
<p>第三点说如果thisArg为非对象类型,则会强制转型成对象类型。</p>
<pre><code>function foo(){
console.log(this);
}
var aa = 2;
console.log(aa);//输出2
foo.call(aa);//输出 Number
</code></pre>
<p>这里的输出结果分别为2和Number,它将基本类型转型成了对象包装类型。</p>
<p>第四点说明剩下的情况thisArg和this为一一对应的关系。</p>
<p>规范里面对this指向的描述还是比较明确的。只要你搞清楚thisArg怎么确定,thisArg和this的对应关系,那么你就能搞定所有this的情况了。</p>
<h3>确保this的指向</h3>
<p>在实际使用this的过程中,遇到得最多得一个问题可能就是上下文丢失的问题了。因为javascript中的函数是可以作为参数传递的,那么其他对象在执行回调函数时就可能造成回调函数原来的上下文丢失,也就是this的指向改变了。</p>
<pre><code>var C = function(){
this.name = 'test';
this.greet = function(){
console.log('Hello,I am '+this.name+'!');
};
}
var obj = new C();
obj.greet();//输出 Hello,I am test!
setTimeout(obj.greet, 1000);//输出 Hello,I am !</code></pre>
<p>可见第二条输出中this的值改变了,其实我们是希望this能够指向obj的。解决该问题的方法有两种。</p>
<p>1.bind方法。<br>bind方法通过闭包巧妙地实现了上下文的绑定,它实际上是将原方法包装成了一个新方法。一般的实现如下:</p>
<pre><code>Function.prototype.bind = function(){
var args = arguments,
thisArg = arguments[0],
func = this;
return function(){
var arg = Array.prototype.slice.call(args, 1);
Array.prototype.push.apply(args, arguments);
return func.apply(thisArg, arg);
}
}</code></pre>
<p>前面的示例代码我们只需要加上bind,就能够得到我们希望的结果了。</p>
<pre><code>...
setTimeout(obj.greet.bind(obj), 1000);//输出 Hello,I am test!
...</code></pre>
<p>2.es6箭头函数。<br>es6里面提供了一个新的语法糖,箭头函数。箭头函数的this不再变幻莫测,它永远指向函数定义时的this值。</p>
<pre><code>var C = function(){
this.name = 'test';
this.greet = ()=>{
console.log('Hello,I am '+this.name+'!');
};
}
var obj = new C();
obj.greet();//输出 Hello,I am test!
setTimeout(obj.greet, 1000);//输出 Hello,I am test!</code></pre>
<p>我们将前面的示例该成箭头函数后,两处的输出结果一样了。this的值不再改变了,这是我们想要的。</p>
<h3>小结</h3>
<p>this看起来是个非常小的知识点,其实挖起来还是有很多细节的,特别是规范里面的一些定义,本人觉得对于一个js程序员来说是非常重要的。本人后面也准备找些ecma规范里面的知识点和大家分享,希望能对大家有所帮助。由于本人能力有限,如有理解错误的地方还望指出。</p>
通过类型继承深入理解原型继承
https://segmentfault.com/a/1190000003879113
2015-10-20T01:12:47+08:00
2015-10-20T01:12:47+08:00
crossea
https://segmentfault.com/u/crossea
2
<p>基于类的继承是大多数人所熟悉的,也是比较容易理解的。当我们形成类型继承的思维定势后,再次接触原型继承可能会觉得有些奇怪并难以理解。你更可能会吐槽,原型继承根本就不能叫做继承,一点都不面向对象。本人最初也是这样认为的,但深入仔细的对比后发现,两者其实并没有本质的差别,只是表面有点不一样而已。且看下面的分析。</p>
<h3>类型继承</h3>
<p>先看一个类型继承的例子,代码如下:</p>
<pre><code>public class A {
//...
}
public class B extends A {
//...
}
public class C extends B {
//...
}
C c = new C();</code></pre>
<p>A、B、C为三个继承关系的类,最后将类C实例化。下面这张图描述了类和实例的对应关系。左边为类,右边为其对应实例。</p>
<p><img src="http://p4.music.126.net/y5tn7FBRfLwOr8VPDMBvZw==/3343614861351759.jpg" alt="图片1" title="图片1"></p>
<p>我们看到,类C实例化后,内存中不仅存在c对象,同时还有a、b两个对象。因为在java中,当我们在执行new C()操作时,jvm中会发生如下过程:</p>
<ol>
<li><p>创建A的实例a。</p></li>
<li><p>创建B的实例b,并将实例b的super指针指向a。</p></li>
<li><p>创建C的实例c,并将实例c的super指针指向b。</p></li>
</ol>
<p>过程1和过程2对用户是透明的,不需要人工干预,引擎会按照“蓝图”把这两个过程完成。通过上图右半部分我们可以看到,super指针将a、b、c三个实例串起来了,这里是实现继承的关键。当我们在使用实例c的某个属性或方法时,若实例c中不存在则会沿着super指针向父类对象查找,直到找到,找不到则出错。这就是继承能够达到复用目的内部机制。看到这里大家或许已经联想到原型链了,super所串起来的这个链几乎和原型链一样,只是叫法不一样而已。下面我们就来看看原型继承。</p>
<h3>原型继承</h3>
<p><img src="http://p4.music.126.net/wEl9kzLABwZxywJAX5lb5g==/3437073349386474.jpg" alt="图片2" title="图片2"></p>
<p>上面是原型继承的示意图。先看图的右半部分,__proto__指针形成的对象链就是原型链。__proto__是一个私有属性,只能看不准访问(某些浏览器看也不给看)。__proto__的作用和前面的super是一样的,原型链实现复用的机制和类型继承也几乎是一样的,这里不再重复。有一点不一样就是原型继承中的属性写操作只会改变当前对象并不会影响原型链上的对象。</p>
<p>如何去构造原型链呢?看上去要稍微麻烦一些。原型继承里面没有类的概念,我们需要通过代码,手动完成这个过程。上图中的A、B、C在原型继承称作构造器。构造器就是一个普通的函数,但是将new操作符用到构造器上时,它会执行一个叫[[construct]]的过程。大致如下:</p>
<ol>
<li><p>创建一个空对象obj。</p></li>
<li><p>设置obj的内部属性[[Class]]为Object。</p></li>
<li><p>设置obj的内部属性[[Extensible]]为true。</p></li>
<li><p><strong>设置obj的[[__proto__]]属性:如果函数对象prototype的值为对象则直接赋给obj,否则赋予Object的prototype值</strong>。</p></li>
<li><p>调用函数对象的[[Call]]方法并将结果赋给result。</p></li>
<li><p>如果result为对象则返回result,否则返回obj。</p></li>
</ol>
<p>从第4条可以看到,构造器生成的对象的__proto__属性会指向构造器的prototype值,这就是我们构造原型链的关键。下面的代码是上图原型链的构造过程。</p>
<pre><code>function A(){
//...
}
function B(){
//...
}
function C(){
//...
}
var a = new A();
B.prototype = a;
var b = new B();
C.prototype = b;
var c = new C();</code></pre>
<p>上述代码虽然能达到目的,但有点繁琐,我们可以将这个过程封装一下。backbone的实现是这样的:</p>
<pre><code>var extend = function(protoProps, staticProps) {
var parent = this;
var child;
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
_.extend(child, parent, staticProps);
child.prototype = _.create(parent.prototype, protoProps);
child.prototype.constructor = child;
child.__super__ = parent.prototype;
return child;
}</code></pre>
<p>其中_.extend(child, parent, staticProps)是将staticProps和parent对象的属性复制给child。_.create方法的实现大概如下。</p>
<pre><code>_.create = function(prototype, protoProps){
var F = function(){};
F.prototype = prototype;
var result = new F();
return _.extend(result, protoProps);
}</code></pre>
<p>有了extend方法,我们的代码就可以写成:</p>
<pre><code> A.extend = extend;
var B = A.extend({
//...
);
var C = B.extend({
//...
);
var c = new C();</code></pre>
<p>这段代码和类型继承的代码十分相似,通过原型继承我们也可以达到类型继承的效果。但是通过前面的比较我们发现,继承的本质就其实就是对象的复用。原型继承本身就是以对象为出发点考虑的,所以大多时候我们并不一定要按照类型继承的思维考虑问题。而且js是弱类型,对象的操作也极其自由,上述的_.create方法可能是js里面实现继承的一个更简单有效的方法。</p>
<h3>总结</h3>
<p>前面讨论了两种继承方式,可以看到,继承的本质其实就是对象的复用。本人觉得原型继承更加的简单和明确,它直接就是从对象的角度考虑问题。当然,如果你需要一个非常强大的继承体系,你也可以构造出一个类似类型继承的模式。相对来说,本人觉得原型继承更灵活和自由些,也是非常巧妙和独特的。</p>