[译] 为什么原型继承很重要 – SegmentFault

原文:http://segmentfault.com/a/1190000002596600

五天之前我写了一个关于ES6标准中Class的文章。在里面我介绍了如何用现有的Javascript来模拟类并且介绍了ES6中类的用法,其实它只是一个语法糖。感谢Om Shakar以及Javascript Room中的各位,我的编程风格从那时候开始发生了改变;就像Dougla Crockford2006年做的一样,我也学习了很多来完全理解基于原型的编程方式。

Javascript是一个多样化的编程语言。它拥有面向对象和函数式的编程特点,你可以使用任何一种风格来编写代码。然而这两个编程风格并不能很好的融合。例如,你不无法同时使用new(典型的面向对象的特点)和apply(函数式编程的特点).原型继承一直都作为连接这两种风格的桥梁。

基于类继承的问题

大部分Javascript程序员会告诉你基于类的继承不好。然而它们中只有很少一部分知道其中的原因。事实实际上是基于类的基础并没有什么不好。Python是基于类继承的,并且它是一门很好的编程语言。但是,基于类的继承并不适合用于Javascript。Python正确的使用了类,它们只有简单的工厂方法不能当成构造函数使用。而在Javascript中任何函数都可以被当成构造函数使用。

Javascript中的问题是由于每个函数都可以被当成构造函数使用,所以我们需要区分普通的函数调用和构造函数调用;我们一般使用new关键字来进行区别。然而,这样就破坏了Javascript中的函数式特点,因为new是一个关键字而不是函数。因而函数式的特点无法和对象实例化一起使用。

function Person(firstname,lastname){
    this.firstname = firstname ;
    this.lastname = lastname ;
}

考虑上面这段程序。你可以通过new关键字来调用Person方法来创建一个函数Person的实例:

var author = new Person('Aadit','Shah') ;

然而,没有任何办法来使用apply方法来为构造函数指定参数列表:

var author = new Person.apply(null,['Aadit','Shah']);//error

但是,如果new是一个方法那么上面的需求就可以通过下面这种方式实现了:

var author = Person.new.apply(Person,['Aadit','Shah']) ;

幸运的是,因为Javascript有原型继承,所以我们可以实现一个new的函数:

Function.prototype.new = function () {
    function functor() { return constructor.apply(this, args); }
    var args = Array.prototype.slice.call(arguments);
    functor.prototype = this.prototype;
    var constructor = this;
    return new functor;
};

在像Java这样对象只能通过new关键字来实例化的语言中,上面这种方式是不可能实现的。

下面这张表列出了原型继承相比于基于类的基础的优点:

基于类的继承 原型继承
类是不可变的。在运行时,你无法修改或者添加新的方法 原型是灵活的。它们可以是不可变的也可以是可变的
类可能会不支持多重继承 对象可以继承多个原型对象
基于类的继承比较复杂。你需要使用抽象类,接口和final类等等 原型继承比较简洁。你只有对象,你只需要对对象进行扩展就可以了

不要再使用关键词new了

到现在你应该知道为什么我觉得new关键字是不会的了吧—你不能把它和函数式特点混合使用。然后,这并不代表你应该停止使用它。new关键字有合理的用处。但是我仍然建议你不要再使用它了。new关键字掩盖了Javascript中真正的原型继承,使得它更像是基于类的继承。就像Raynos说的:

new是Javascript在为了获得流行度而加入与Java类似的语法时期留下来的一个残留物

Javascript是一个源于Self的基于原型的语言。然而,为了市场需求,Brendan Eich把它当成Java的小兄弟推出:

并且我们当时把Javascript当成Java的一个小兄弟,就像在微软语言家庭中Visual Basic相对于C++一样。

这个设计决策导致了new的问题。当人们看到Javascript中的new关键字,他们就想到类,然后当他们使用继承时就遇到了傻了。就像Douglas Crockford说的:

这个间接的行为是为了使传统的程序员对这门语言更熟悉,但是却失败了,就像我们看到的很少Java程序员选择了Javascript。Javascript的构造模式并没有吸引传统的人群。它也掩盖了Javascript基于原型的本质。结果就是,很少的程序员知道如何高效的使用这门语言

因此我建议停止使用new关键字。Javascript在传统面向对象假象下面有着更加强大的原型系统。然大部分程序员并没有看见这些还处于黑暗中。

理解原型继承

原型继承很简单。在基于原型的语言中你只有对象。没有类。有两种方式来创建一个新对象—“无中生有”对象创建法或者通过现有对象创建。在Javascript中Object.create方法用来创建新的对象。新的对象之后会通过新的属性进行扩展。

“无中生有”对象创建法

Javascript中的Object.create方法用来从0开始创建一个对象,像下面这样:

var object = Object.create(null) ;

上面例子中新创建的object没有任何属性。

克隆一个现有的对象

Object.create方法也可以克隆一个现有的对象,像下面这样:

var rectangle = {
    area : function(){
        return this.width * this.height ;
    }
} ;
var rect = Object.create(rectangle) ;
上面例子中rectrectangle中继承了area方法。同时注意到rectangle是一个对象字面量。对象字面量是一个简洁的方法用来创建一个Object.prototype的克隆然后用新的属性来扩展它。它等价于:
var rectangle = {
    area : function(){
        return this.width * this.height ;
    }
} ;
var rect = Object.create(rectangle) ;

扩展一个新创建的对象

上面的例子中我们克隆了rectangle对象命名为rect,但是在我们使用rectarea方法之前我们需要扩展它的widthheight属性,像下面这样:

rect.width = 5 ;
rect.height = 10 ;
alert(rect.area()) ;

然而这种方式来创建一个对象的克隆然后扩展它是一个非常傻缺的方法。我们需要在每个rectangle对象的克隆上手动定义widthheight属性。如果有一个方法能够为我们来完成这些工作就很好了。是不是听起来有点熟悉?确实是。我要来说说构造函数。我们把这个函数叫做create然后在rectangle对象上定义它:

var rectangle = {
    create : function(width,height){
        var self = Object.create(this) ;
        self.height = height ;
        self.width = width ;
        return self ;
    } ,
    area : function(){
        return this.width * this.height ;
    }
} ;
var rect = rectangle.create(5,10) ;
alert(rect.area()) ;

构造函数 VS 原型

等等。这看起来很像Javascript中的正常构造模式:

function Rectangle(width, height) {
    this.height = height;
    this.width = width;
} ;

Rectangle.prototype.area = function () {
    return this.width * this.height;
};

var rect = new Rectangle(5, 10);

alert(rect.area());
是的,确实很像。为了使得Javascript看起来更像Java原型模式被迫屈服于构造模式。因此每个Javascript中的函数都有一个prototype对象然后可以用来作为构造器(这里构造器的意思应该是说新的对象是在prototype对象的基础上进行构造的)。new关键字允许我们把函数当做构造函数使用。它会克隆构造函数的prototype属性然后把它绑定到this对象中,如果没有显式返回对象则会返回this

原型模式和构造模式都是平等的。因此你也许会怀疑为什么有人会困扰于是否应该使用原型模式而不是构造模式。毕竟构造模式比原型模式更加简洁。但是原型模式相比构造模式有许多优势。具体如下:

构造模式 原型模式
函数式特点无法与new关键字一起使用 函数式特点可以与create结合使用
忘记使用new会导致无法预期的bug并且会污染全局变量 由于create是一个函数,所以程序总是会按照预期工作
使用构造函数的原型继承比较复杂并且混乱 使用原型的原型继承简洁易懂

最后一点可能需要解释一下。使用构造函数的原型继承相比使用原型的原型继承更加复杂,我们先看看使用原型的原型继承:

var square = Object.create(rectangle);
square.create = function (side) {
    return rectangle.create.call(this, side, side);
} ;
var sq = square.create(5) ;
alert(sq.area()) ;

上面的代码很容易理解。首先我们创建一个rectangle的克隆然后命名为square。接着我们用新的create方法重写square对象的create方法。最终我们从新的create方法中调用rectanglecreate函数并且返回对象。相反的,使用构造函数的原型继承像下面这样:

function Square(){
    Rectangle.call(this,side,side) ;
} ;

Square.prototype = Object.create(Rectangle.prototype) ;

Square.prototype.constructor = Square ;

var sq = new Square(5) ;

alert(sq.area()) ;

当然,构造函数的方式更简单。然后这样的话,向一个不了解情况的人解释原型继承就变得非常困难。如果想一个了解类继承的人解释则会更加困难。

当使用原型模式时一个对象继承自另一个对象就变得很明显。当使用方法构造模式时就没有这么明显,因为你需要根据其他构造函数来考虑构造继承。

对象创建和扩展相结合

在上面的例子中我们创建一个rectangle的克隆然后命名为square。然后我们利用新的create属性扩展它,重写继承自rectangle对象的create方法。如果把这两个操作合并成一个就很好了,就像对象字面量是用来创建Object.prototype的克隆然后用新的属性扩展它。这个操作叫做extend,可以像下面这样实现:

Object.prototype.extend = function(extension){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;

    for(var property in extension){
        if(hasOwnProperty.call(extension,property) ||
            typeof obejct[property] === 'undefined')
            //这段代码有问题,按照文章意思,这里应该使用深复制,而不是简单的浅复制,deepClone(extension[property],object[property]),deepClone的实现可以看我之前关于继承的博客
            object[properyty] = extension[property] ;
    }
    return object ;
} ;

译者注:我觉得博主这里的实现有点不符合逻辑,正常extend的实现应该是可以配置当被扩展对象和用来扩展的对象属性重复时是否覆盖原有属性,而博主的实现就只是简单的覆盖。同时博主的实现在if判断中的做法个人觉得是值得学习的,首先判断extension属性是否是对象自身的,如果是就直接复制到object上,否则再判断object上是否有这个属性,如果没有那么也会把属性复制到object上,这种实现的结果就使得被扩展的对象不仅仅只扩展了extension中的属性,还包括了extension原型中的属性。不难理解,extension原型中的属性会在extension中表现出来,所以它们也应该作为extension所具有的特性而被用来扩展object。所以我对这个方法进行了改写:

 Object.prototype.extend = function(extension,override){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    for(var property in extension){
        if(hasOwnProperty.call(extension,property) || 
            typeof object[property] === 'undefined'){
            if(object[property] !== 'undefined'){
                if(override){
                    deepClone(extension[property],object[property]) ;
                }
            }else{
                deepClone(extension[property],object[property]) ;
            }    
        }
    }
};

利用上面的extend方法,我们可以重写square的代码:

var square = rectangle.extend({
    create : function(side){
        return rectangle.create.call(this,side,side) ;
    }
}) ;

var sq = square.create(5) ;
alert(sq.area()) ;

extend方法是原型继承中唯一需要的操作。它是Object.create函数的超集,因此它可以用在对象的创建和扩展上。因此我们可以用extend来重写rectangle,使得create函数更加结构化看起来就像模块模式

var rectangle = {
    create : function(width,height){
        return this.extend({
            height : height ,
            width : width
        }) ;
    }
} ;

var rect = rectangle.create(5,10) ;
alert(rect.area()) ;

原型继承的两种方法

一些人可能已经注意到extend函数返回的对象实际上是继承了两个对象的属性,一个是被扩展的对象,另一个是用来扩展的对象。另外从两个对象继承属性的方式也不一样。第一种情况下是通过委派来继承属性(也就是使用Object.create()来继承属性),第二种情况下使用合并属性的方式来继承属性。

委派(差异化继承)

很多Javascript程序员对于差别继承比较熟悉。维基百科是这么解释的:

大部分对象是从其他更一般的对象中得到的,只是在一些很小的地方进行了修改。每个对象通常在内部维护一个指向其他对象的引用列表,这些对象就是该对象本身进行差异化继承的对象。

Javascript中的原型继承是基于差异化继承的。每个对象都有个内部指针叫做[[proto]] (在大部分浏览器中可以通过__proto__属性访问),这个指针指向对象的原型。多个对象之间通过内部[[proto]]属性链接起来形成了原型链,链的最后指向null

当你试图获取一个对象的属性时Javascript引擎会首先查找对象自身的属性。如果在对象上没找到该属性,那么它就会去对象的原型中去查找。以此类推,它会沿着原型链一直查找知道找到或者到原型链的末尾。

function get(object,property){
    if(!Object.hasOwnProperty.call(object,property)){
        var prototype = Object.getPrototypeOf(object) ;
        if(prototype) return get(prototype,property) ;
    }else{
        return object[property] ;
    }
} ;

Javascript中属性查找的过程就像上面的程序那样。

克隆(合并式继承)

大多数Javascript程序员会觉得复制一个对象的属性到另一个对象上并不是一个正确的继承的方式,因为任何对原始对象的修改都不会反映在克隆的对象上。五天前我会同意这个观点。然而现在我相信合并式继承是原型继承的一种正确方式。对于原始对象的修改可以发送到它的副本来实现真正的原型继承。

合并式继承和代理有他们的优点和缺点。下表列出了它们的优缺点:

代理 合并
任何对于原型的修改都会反映在所有副本上 任何对于原型的修改都需要手动更新到副本中
属性查找效率较低因为需要进行原型链查找 属性查找更搞笑因为继承的属性是通过复制的方式附加在对象本身的
使用Object.create()方法只能继承单一对象 对象可以从任意数量的对象中通过复制继承属性

从多个原型继承

上表中最后一点告诉我们对象可以通过合并的方式从多个原型中继承属性。这是一个重要的特点因为这证明原型继承比Java中的类继承更强大并且与C++中的类继承一样强大。为了实现多重继承,你只需要修改extend方法来从多个原型中复制属性。

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ;
    var object = Object.create(this) ;
    var length = arguments.length ;
    var index = length ;

    while(index){
        var extension = arguments[length - (index--)] ;
        for(var property in extension){
            if(hasOwnProperty.call(extension,property)||
                typeof object[property] === 'undefined'){
                //这里同样应该使用深复制
                object[property] = extension[property] ;
            }
        }
    }
    return object;
} ;

多重继承是非常有用的因为它提高了代码的可重用性和模块化。对象通过委派继承一个原型对象然后通过合并继承其他属性。比如说你有一个事件发射器的原型,像下面这样:

var eventEmitter = {
    on : function(event,listener){
        if(typeof this[event] !== 'undefined')
            this[event].push(listener) ;
        else
            this[event] = [listener] ;
    } ,
    emit : function(event){
        if(typeof this[event] !== 'undefined'){
            var listeners = this[event] ;
            var length = listeners.length,index = length ;
            var args = Array.prototype.slice.call(arguments,1) ;

            while(index){
                var listener = listeners[length - (index--)] ;
                listener.apply(this,args) ;
            }
        }
    }
} ;

现在你希望square表现得像一个事件发射器。因为square已经通过委派的方式继承了rectangle,所以它必须通过合并的方式继承eventEmitter。这个修改可以很容易地通过使用extend方法实现:

var square = rectangle.extend(eventEmitter,{
    create : function(side){
        return rectangle.create.call(this,side,side) ;
    } ,
    resize : function(newSize){
        var oldSize = this.width ;
        this.width = this.height = newSize ;
        this.emit('resize',oldSize,newSize) ;
    }
}) ;
var sq = square.create(5) ;
sq.on('resize',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

sq.resize(10) ;
alert(sq.area()) ;

在Java中是不可能实现上面的程序的,因为它不支持多重继承。相应的你必须另外再创建一个EventEmitter类或者使用一个EventEmitter接口并且在每个实现该接口的类中分别实现onemit方法。当然你在C++中不需要面对这个问题。我们都知道Java sucks(呵呵呵)。

Mixin的蓝图(Buleprint)

在上面的例子中你肯定注意到eventEmitter原型并没有一个create方法。这是因为你不应该直接创建一个eventEmitter对象。相反eventEmitter是用来作为其他原型的原型。这类原型称为mixin。它们等价于抽象类。mixin用来通过提供一系列可重用的方法来扩展对象的功能。

然而有时候mixin需要私有的状态。例如eventEmitter如果能够把它的事件监听者列表放在私有变量中而不是放在this对象上会安全得多。但是mixin没有create方法来封装私有状态。因此我们需要为mixin创建一个蓝图(blueprint)来创建闭包。蓝图(blueprint)看起来会像是构造函数但是它们并不用像构造函数那样使用。例如:

function eventEmitter(){
    var evnets = Object.create(null) ;

    this.on = function(event,listener){
        if(typeof events[event] !== 'undefined')
            events[event].push(listener) ;
        else
            events[event] = [listener] ;
    } ;
    this.emit = function(event){
        if(typeof events[event] !== 'undefined'){
            var listeners = events[event] ;
            var length = listeners.length ,index = length ;
            var args = Array.prototype.slice.call(arguments,1) ;
        }
    } ;
} ;

一个蓝图用来在一个对象创建之后通过合并来扩展它(我觉得有点像装饰者模式)。Eric Elliot把它们叫做闭包原型。我们可以使用蓝图版本的eventEmitter来重写square的代码,如下:

var square = rectangle.extend({
    create : function(side){
        var self = rectangle.create.call(this,side,side) ;
        eventEmitter.call(self) ;
        return self ;
    } ,
    resize : function(newSize){
        var oldSize = this.width ;
        this.width = this.height = newSize ;
        this.emit('resize',oldSize,newSize) ;
    }
}) ;
var sq = square.create(5) ;

sq.on('resize',function(oldSize,newSize){
    alert('sq resized from ' + oldSize + 'to' + newSize + '.') ;
}) ;

sq.resize(10) ;

alert(sq.area()) ;

蓝图在Javascript中是独一无二的。它是一个很强大的特性。然而它们也有自己的缺点。下表列出了mixin和蓝图的优缺点:

Mixin 蓝图
它们用来扩展对象的原型。因此对象共享同一个原型 它们用来扩展新创建的对象。因此每个对象都是在自己对象本身进行修改
因为缺少封装方法所以不存在私有状态 它们是函数,所以可以封装私有状态
它们是静态原型并且不能被自定义 它们可以传递参数来自定义对象,可以向蓝图函数传递一些用来自定义的参数

修复instanceof操作

许多Javascript程序员会觉得使用原型模式来继承违背了语言的精髓。他们更偏向于构造模式因为他们觉得通过构造函数创建的对象才是真正的实例,因为instanceof操作会返回true。然而,这个争论是没有意义的,因为instanceof操作可以像下面这样实现:

Object.prototype.instanceof = function(prototype){
    var object = this ;
    do{
        if(object === prototype) return true ;
        var object = Object.getPrototypeOf(object) ;
    }while(object) ;
    return false ;
}

这个instanceof方法现在可以被用来测试一个对象是否是通过委派从一个原型继承的。例如:

sq.instanceof(square) ;

然而还是没有办法判断一个对象是否是通过合并的方式从一个原型继承的,因为实例的关联信息丢失了。为了解决这个问题我们将一个原型的所有克隆的引用保存在原型自身中,然后使用这个信息来判断一个对象是否是一个原型的实例。这个可以通过修改extend方法来实现:

Object.prototype.extend = function(){
    var hasOwnProperty = Object.hasOwnProperty ; 
    var object = Object.create(this) ;
    var length = arguments.lenght ;
    var index = length ;

    while(index){
        var extension = arguments[length - (index--)] ;

        for(var property in extension){
            if(property !== 'clones' &&
                hasOwnProperty.call(extension,property) ||
                typeof object[property] === 'undefined')
                object[property] = extension[property] ;

        if(hasOwnProperty.call(extension,'clones')})
            extension.clones.unshift(object) ;
        else
            extension.clones = [object] ;
        }
    }
    return object;
} ;

通过合并继承自原型的对象形成了一个克隆树,这些树从根对象开始然后向下一直到叶子对象。一个克隆链是一个从根对象到叶子对象的单一路径,这跟遍历原型链很相似。我们可以使用这个信息来判断一个对象是否是通过合并继承自一个原型。

Object.prototype.instanceof = function(prototype){
    if (Object.hasOwnProperty.call(prototype, "clones"))
        var clones = prototype.clones;
    var object = this;

    do {
        if (object === prototype ||
            clones && clones.indexOf(object) >= 0)
            return true;
        var object = Object.getPrototypeOf(o  bject);
    } while (object);

    return false;
} ;
这个instanceof方法现在可以用来判断一个对象是否是通过合并继承自一个原型。例如:
sq.instanceof(eventEmitter);

在上面的程序中instanceof会返回true如果我妈使用mixin版本的eventEmitter。然而如果我们使用蓝图版本的eventEmitter它会返回false。为了解决这个问题我创建了一个蓝图函数,这个函数接收一个蓝图作为参数,向它添加一个clones属性然后返回一个记录了它的克隆的新蓝图:

function blueprint(f){
    var g = function(){
        f.apply(this,arguments) ;
        g.clones.unshift(this) ;
    } ;
    g.clones = [] ;
    return g ;
} ;
var eventEmitter = blueprint(function(){
    var events = Object.create(null);
    this.on = function (event, listener) {
        if (typeof events[event] !== "undefined")
            events[event].push(listener);
        else events[event] = [listener];
    };

    this.emit = function (event) {
        if (typeof events[event] !== "undefined") {
            var listeners = events[event];
            var length = listeners.length, index = length;
            var args = Array.prototype.slice.call(arguments, 1);

            while (index) {
                var listener = listeners[length - (index--)];
                listener.apply(this, args);
            }
        }
    };
}) ;

向原型发送变化

上面例子中的clones属性有双重作用。它可以用来判断一个对象是否是通过合并继承自一个原型的,然后他可以用来发送原型改变给所有它的克隆。原型继承相比类继承最大的优势就是你可以修改一个原型在它创建之后。为了使克隆可以继承对于原型的修改,我们创建了一个叫做define的函数:

Object.prototype.define = function (property, value) {
    this[property] = value;

    if (Object.hasOwnProperty.call(this, "clones")) {
        var clones = this.clones;
        var length = clones.length;

        while (length) {
            var clone = clones[--length];
            if (typeof clone[property] === "undefined")
                clone.define(property, value);
        }
    }
};
现在我们可以修改原型然后这个修改会反映在所有的克隆上。例如我们可以创建创建一个别名addEventListener针对eventEmitter上的on方法:
var square = rectangle.extend(eventEmitter, {
    create: function (side) {
        return rectangle.create.call(this, side, side);
    },
    resize: function (newSize) {
        var oldSize = this.width;
        this.width = this.height = newSize;
        this.emit("resize", oldSize, newSize);
    }
});

var sq = square.create(5);

eventEmitter.define("addEventListener", eventEmitter.on);

sq.addEventListener("resize", function (oldSize, newSize) {
    alert("sq resized from " + oldSize + " to " + newSize + ".");
});

sq.resize(10);

alert(sq.area());

蓝图需要特别注意。尽管对于蓝图的修改会被发送到它的克隆,但是蓝图的新的克隆并不会反映这些修改。幸运的是这个问题的解决方法很简单。我们只需要对blueprint方法进行小小的修改,然后任何对于蓝图的修改就会反映在克隆上了。

function blueprint(f) {
    var g = function () {
        f.apply(this, arguments);
        g.clones.unshift(this);

        var hasOwnProperty = Object.hasOwnProperty;

        for (var property in g)
            if (property !== "clones" &&
                hasOwnProperty.call(g, property))
                    this[property] = g[property];
    };

    g.clones = [];

    return g;
};

结论

恭喜你。如果你读完了整篇文章并且理解了我所说的东西,你现在就了解了 原型继承并且为什么它很重要。很感谢你们看完了这篇文章。我希望这个博客能帮到你们。原型继承是强大的并且值得更多的信任。然后大部分人从来不明白这个因为Javascript中的原型继承被构造模式所掩盖了。

译者注

这篇文章针对几种继承方式进行了对比。文章中说到的几种扩展的方法我觉得是比较有用的。蓝图(blueprint,这个实在不知道该怎么翻译)的扩展方式比较像设计模式中的装饰者模式,通过函数对对象进行扩展,这个是一种比较好玩的扩展方式,可以跟原型继承配合使用。另外文中提到了new关键字的弊端,个人觉得主要的原因还是new关键字的出现掩盖了Javascript本身原型继承的特点,人们自然而然就会想到传统的类继承,这样就无法发挥原型继承的最大威力。最后说到的属性修改传播的问题也挺有意思的,应该会有相应的应用场景。总之,我觉得原型继承相比于传统的类继承提供了更大的灵活性,可以给我们开发者提供很大的发挥空间,不过不管怎样,到最后还是要涉及到基本的原型继承的原理上,所以掌握了原型继承的原理就可以根据不同的应用场景使用各种各样的扩展方式。


原文地址:http://aaditmshah.github.io/why-prototypal-inheritance-matters/

Categories: JavaScript

微软推出ManifoldJS,Web App自动转成各平台本地App-CSDN.NET

微软推出开源工具ManifoldJS,可以自动将Web App转换成各种平台的App(安卓、iOS、Chrome OS、Windows)。

ManifoldJS通过获取网站meta-data信息,即可产生针对Android、ChromeOS、FirefoxOS,iOS以及Windows各平台的“本地”应用。号称创建跨平台、跨设备本地应用最简单的方法。

开发者在构建过程中,运行以下代码,即可使用ManifoldJS:

[js] view plaincopy

$ npm install manifoldjs

$ grunt manifoldjs

ManifoldJS基于最新W3C Web应用清单标准工作草案(W3C Manifest working draft),同时会保持更新,保证开发者的应用可以持续工作。

GitHub:https://github.com/manifoldjs

阅读详情 -> 微软推出ManifoldJS,Web App自动转成各平台本地App-CSDN.NET.

Categories: 未分类

React.js 要点

阅读详情 -> React.js 要点.

React.js 要点

最近在公司的一个移动端WEB产品中使用了React这个框架(并不是React-Native),记录一下在开发过程中遇到的各种问题以及对应的解决方法,希望能对读者有所帮助。

React原则

React不多做介绍,就是Facebook的一个开源JS框架,专注的层面为View层,不包括数据访问层或者那种Hash路由(不过React有插件支持),与Angularjs,Emberjs等大而全的框架不同,React专注的中心是Component,即组件,React认为一切页面元素都可以抽象成组件,比如一个表单,或者表单中的某一项。

常用技术要点

props

每一个组件都有可变与不可变的属性,props就是不可变的属性,当props改变时,组件的绘制方法不会被调用。

states

states就是组件的可变属性,states的改变会触发组件的render函数,react与其他mvvm框架的最大的不同点就是,react组件可以想象成一个状态机,状态的改变会重绘UI,然后根据 dom diff 算法来绘制UI,而其他的mvvm框架则是对js对象的dirty check(Angularjs)或者defineProperty时指定的回调函数(Emberjs),因此,状态机制与dom diff的存在声称React更快。

我们在设计React组件时如何灵活的运用props与state是一个非常关键的要点。

refs

refs这个点主要用在上级组件调用下级组件的场景,这个场景非常多见。

componentDidMount

组件UI渲染完毕后调用,常用在这种业务场景:调用ajax获得数据后,渲染UI。

state的改变会触发render

正如上文中所说,React组件是一个一个状态机,state的改变会改变状态,会触发重绘,对于state的设计是对React组件设计的关键。

各种坑

触摸事件

React中的触摸事件仅用三种,touchstart, touchend, touchmove,可是这种会有问题,有时候我需要滚动页面的时候,很容易触发某一个元素的touchend事件,为此笔者找了一个React第三方组件,React-tappable

还无意中发现一个Android与iOS的不同之处

input在disable且readonly之后,onClick会在iOS上触发不起来,onTouchEnd又会在Android上把键盘弹出来,这边笔者做了个Hack,iOS下用onTouchEnd,Android下用onClick,就正常了。

高级技术要点

mixin

 

曾经跟一个朋友讨论,封装utils模块为纯js模块好还是React式的模块好的问题,他给我了一个概念,React mixin,本来自己在开发中确实写了很多可复用的小组件,但由于没有过多的了解React周边的相关知识,因此忽略了React mixin,笔者认为以后可以探究一下这个技术点。

Categories: JavaScript

关于CSS[几乎]没人知道的3件事 – 前端外刊评论 – 知乎专栏

你了解 CSS 吗?在六个月前,我提供了一个在线免费 CSS 测试系统。测试结果表明很多一线开发者并没有如他们所想的那样了解 CSS。目前有超过 3,000 人参加了该项测试,平均成绩只有 55 分。

但是,嘿,平均分本身并没有什么意思。我更加关心大家都栽到了哪些问题上。这篇文章中,按照出错的程度将其中三个问题列出来。我会和你讨论每个问题,告诉你哪个答案被选择的最多,然后解释正确答案。

可以肯定地说,如果你读完这篇文章后参加测试,将会有不公平的优势!

Q1:设置 line-height 的最佳方式

第一个问题对于有文本样式常规操作经验的开发者来说应该很简单:

想要让站点内文字默认为双倍行距。下面哪个 line-height 值是最佳实现方式?

  • 200%
  • 2em
  • 2
  • double

对于这4个答案,你可能觉得仅仅凭运气也会有 25% 的人会答对,但仅仅 31% 的人回答正确!花上一分钟时间选择你认为的正确答案,然后继续。

首先排除,double 只是用来混淆你。 line-height 接受的唯一关键字是 normal。我很高兴地说,仅仅有 9% 被这个选项蒙蔽。尽管,剩下的三个答案都非常得普遍。

大多数测试者选择了 2em(39%)。实际上,2em 确实会将它所应用的元素内的文本渲染为双倍行距;不过 200% ,仅仅有 21% 选择了这个答案!或者 em 相比百分数更加流行, 又或者人们根本就没有理解它们。

然而,正确答案是 2。

这是一个很久以前的一个教训,那时我第一次学习 CSS。确保将 line-height 指定为一个无单位的数值;这样一来,指定了不同 font-size 的子元素将会继承这个数值而不是一个固定的高度。

我们假设页面默认的 font-size 是 12pt ,不过它也会包含了一个 font-size 为 24pt 的头部。如果你将 body 的 line-height 设置为2em 或者 200%,那样在文档中(当然也包括头部)就会得到一个 24pt 的行高(body 的 font-size 的 2 倍)。因而,头部就是单倍行距,而不是双倍!

相反,将 line-height 设置为 2 会告知浏览器保持 font-size/line-height 比例,即便文字的尺寸发生变化。body 的行高将是 24pt,而头部的文字为 24pt,行高也将自动增长为 48pt。

Q2:如何让元素部分重叠

这个问题有一点棘手。需要一些 CSS 布局常用到的“奇技淫巧”:

仅仅借助下面的哪个 CSS 属性,可以实现 HTML 元素部分重叠?

  • z-index
  • margin
  • overflow
  • background

请选择一个你认为的正确答案?OK,我们深入看一下。

同样,有一个很容易排除的选项: background。除了 2% 的测试者外都避开了它,知道它控制背景颜色和图像。

很不幸,很多测试者直接选择了 z-index。几乎有 46% 栽倒在这个选项上。我猜测是因为他们没有真正地理解 z-index的工作原理。实际上,自身设置 z-index 属性根本无济于事;你还需要设置元素的 position 属性让 z-index 起作用。简而言之, z-index 控制了部分重叠元素间的堆放顺序,不过前提是它们要重叠。MDN 上有一篇非常棒的文章叫做《理解 CSS z-index》,非常值得仔细阅读。

如果你曾用到过,overflow 同样也很容易排除。它用来在一个固定尺寸的盒子内控制内部元素的行为:是否被遮盖,是否在盒子边框外部展示,等等。而且,这会需要一些其他 CSS 属性所控制的盒子尺寸;仅仅通过它是不会导致部分遮盖的。然而,22% 的测试者认为它可以。

剩下的就是 margin,也就是正确答案。仅仅 30% 的测试者选择了它。你可能好奇究竟一个属性如何做到元素间的相互遮盖。如果你具备一些 CSS 布局的经验,答案应该很明显:负值 margin 让它们相互遮盖。

为了演示这个,我创建了一个仅有两个 div 元素的页面。将第二个 div 的 margin-top 值设置成负值,比如 -100px。砰!现在第二个 div 覆盖了第一个 div 100 像素。

实际上,你几乎不会有意地这样覆盖一个区块,不过负值 margin 在处理外层空间小于 HTML 元素大小的情况时出奇地好用。

对 web 设计历史爱好者来讲,在 2005 年负值 margin 实现的部分覆盖元素使得三列页面布局成为可能,比如所谓的One True Layout(以及后来的圣杯布局)。

Q3:伪元素 VS 伪类

最后一个问题有一点卑劣,我承认。不过只有 23% 的测试者回答正确(比碰运气还糟糕!)。毫无疑问,它挖出了一个疑点。

下面哪个效果可以通过伪元素很好地实现?

  • 当用户将鼠标悬停在超链接上时,为其添加一个投影;

  • 当复选框选中时,在其对应的 label 上显示一种不同的颜色;
  • 为表格的偶数行和奇数行添加不同的背景色;
  • 在弹性布局中,将段落的第一行加粗显示。

其中三个效果需要借助伪类实现;仅仅有一个需要伪元素。你能区分出不同吗?

伪类是一个真实 HTML 元素上的一个特殊的状态。可以认为是浏览器在特定条件下将一个虚拟的类自动应用于某个元素。

伪元素是 HTML 文档的一部分,尽管它不是真实的 HTML 元素,但是 CSS 允许你为它设置样式。就像是虚拟的 HTML 元素——尽管它没有真实的 HTML 标签,但你仍可以为其添加样式。

先记住这点,我们再来看看下面的选项:

当用户将鼠标悬停在超链接上时,为其添加一个投影

超链接是一个真是的 HTML 元素。仅仅在特殊情况下(鼠标悬停)为其应用样式,也就是说我们需要使用伪类。该情况下你应该使用 :hover 伪类。

22% 的测试者误认为这是伪元素。

当复选框选中时,在其对应的 label 上显示一种不同的颜色

一样不对, label 是一个真实的 HTML 元素,不是虚拟的。当复选框被选中时,浏览器会将 :checked 伪类应用于它。然后,你就能够在选择器中使用它为复选框添加样式,甚至和它相邻的 label 元素(比如:使用相邻元素选择器 +)。

20% 的测试者认为这是一个伪元素。

为表格的偶数行和奇数行添加不同的背景色

这着实愚弄了很多人,不过重申一遍我们讨论的是将样式应用于真实的 HTML 元素(在本例中是 tr 元素)。在各自父元素包含的子元素中的偶数或者奇数行的 tr 只是另外一种符合伪类的情景。

在这个示例中,对于偶数行伪类是 :nth-child(even)(或者 :nth-child(2n)),对于奇数行则是 :nth-child(odd) (或者 :nth-child(2n+1))。

我猜测它仅仅因为 :nth-child 和伪元素听起来都和真实的 CSS 特性很相似,但是 36% 的测试者将它选择成伪元素。

在弹性布局中,将段落的第一行加粗显示

很明显,这是正确答案。现在,但愿我们的讨论足够清晰。在弹性布局中,你无法看到页面中的 HTML 代码只能假想“里面仅仅包含段落文本的第一行”。浏览器会根据段落的宽度进行换行,这在弹性布局中是你无法控制的。

:first-line 是允许你将样式应用于文本块第一行的伪元素,无论第一行是在何处换到第二行。

如果你正在想“OK,确实讲得通,但是拜托——没一个人知道伪元素和伪类之间有什么区别”,确实 W3C 也同意你的观点。CSS3 选择器规范中,在区分二者上做了一次尝试,改变了语法——伪元素选择器使用两个冒号(::first-line),而伪类依旧使用一个(:hover)。当然,为了向后兼容,浏览器必须支持这两个版本。

是滴,如我所说:卑劣。不过,如果你和我一样是一个 CSS 骇客,我确信你了解伪元素和伪类的区别。

你做的怎么样?

这就是测试中三个棘手的问题。如果你仅仅对一个有信心,还可以。两个?做得不错。答对了三个,我非常乐意听到!现在,我已经公布了这些问题的答案,同样也可以用这些观点解决更棘手的 CSS 问题。请在评论中留言。

如果你享受这些问题,或许你可以尝试一下测试的其他部分。请放心,其他的问题比这些要简单许多许多!

Kevin Yank 于 1995 年接触 Web 开发,是一位备受尊重的技术类作者。Kev 更是一位知名作家、演说家和 JavaScript 专家。他热衷于让每个人都能轻易地理解 Web 技术。是的,包括你!

原文:3 Things (Almost) No One Knows About CSS

阅读详情 -> 关于CSS[几乎]没人知道的3件事 – 前端外刊评论 – 知乎专栏.

Categories: css3