2016-07-06 00:00:00

Javascript:一次prototype的实践

Javascript进阶必然涉及到prototype,本文以博主一次基于prototype的实践,记录一下用到的相关Javascript的知识.

实践的项目是一个模拟了Adobe Kuler的拾色器,远程仓库在这个链接

prototype

prototype是Javascript的特点之一,它的功能可以用如下语句来描述:

prototype是给类的实例添加属性和方法的方法

prototype最大的用处就是来实现继承.虽然ES6已有了更规范的继承写法,但前端工程师们并没有用上ES6的条件.因此,prototype早已成为了前端的进阶必备知识

prototype是函数独有的属性.一个简单的prototype例子如下:

function foo(){
    this.bar = "bar";
}

foo.prototype.getBar = function(){
    return this.bar;
}

var instance  = new foo();
instance.getBar();
//bar

从继承的角度,可以这样理解如上的代码:

  • foo.prototype包含foo的父类的属性与方法
  • foo()函数为子类的构造器
  • foo.prototype.getBar为foo的父类设置了一个方法
  • 当实例化foo时, foo.prototype中所有的属性和方法全部被此实例所继承

修正构造器中的this

Javascript的new关键字一直被人吐槽,其中一个重要原因是一旦忘记使用new,那么实例化过程就很可能会污染掉全局对象

考虑如下场景

function colorPicker(options){
    this.options = {
        name : "Adobe Color Picker ",
        version : 1.6,

        windowType:"dialog",
    };

    return this
}

这是新手们很容易写出的代码,不能说它是错的,但是可以说它并不完美.

这里不讨论私有变量等问题,关键在于,假如没有使用new,当colorPicker()被调用时,此时this指向的就是全局对象,因此这个函数中实际上将一个内部使用的变量赋值到了全局的options变量中,造成了泄漏.因为有覆盖全局中的重要变量的可能,这将是非常危险的.

因此,可以在构造器中将this指针修正,如下

function colorPicker(inputColour,options){
    if(!(this instanceof colorPicker))
        return new colorPicker(inputColour,options);

    //code
}

这里使用了instanceof关键字来判断是否是生成实例,如果是否的话,则返回一个新的实例,可以理解为深度为一层的递归,用来保证不会污染全局变量.

Javascript所有基本类型都可以不用new而进行实例化,使用的就是这里的方法

prototype方法中的this究竟指向谁?

this关键字一直是新手前端老大难的问题, 那么,在prototype的方法中的this,又是指向谁呢?

考虑如下代码

function colorPicker(){
    this.initSetting()

    return this;
}

colorPicker.prototype.initSetting = function(){
    this.slash = "/";
}

var instance = new colorPicker()

instance.slash  // "/"
colorPicker.prototype.slash // Undefined

可以看到,实例中已经有了slash这个属性,然而prototype中并没有.联合上面修正构造器中的this的内容,可以得出如下结论:

prototype中的this仍然指向当前运行环境(context,又称上下文)

在构造器中调用colorPicker.prototype.initSetting,此时this即指向实例,因此this.slash="/"便给实例进行了赋值操作

原型链与null

考虑如下代码

function colorPicker(){
    this.initSetting()

    return this;
}

colorPicker.prototype.initSetting = function(){
    this.slash = "/";
    return true;
}

var instance = new colorPicker()

for (var i in instance){
      if(instance.hasOwnProperty (i))
        var prefix = "Local property:\t"
      else
        var prefix = "Inherit property:\t"
      console.log(prefix+i)
}

console.log(instance.initSetting())

//Local property:slash
//Inherit property:initSetting
//true

可以从控制台中的输出了解到,slash被认为是实例的ownProperty,而initSetting则不被承认.那么,initSetting又是从哪里查找到的呢?

这就涉及到了Javascript另一个重要概念:原型链

如果读者经常使用控制台,那么应该能非常频繁地看到对象中的__proto__属性.__proto__实际上指向的就是构造器的prototype,这才让我们有能力直接从实例出发,寻找到prototype中的属性和方法.

在实例查找属性,是一个递归的过程:实例本身查找完毕,如果没找到,那么去查实例的__proto__,如果还没有找到,再去查这个__proto____proto__,直到停止.这个从实例到停止中我们访问到的所有对象及其访问顺序就被构成了原型链

等等,那么查找停止的条件是什么呢?那就是:__proto__null

null在Javascript中,代表"这不是一个对象",博主认为null有且只有这个含义.

而"这不是一个对象"的最重要的应用场景,就是原型链的尾端永远为null.它告诉Javascript,这个原型链的尾端已经不是一个对象,应该停止查询了

函数的静态属性和方法

开发过程中,经常需要一些全局变量作为辅助,如果既不愿意污染全局,又不想写入到prototype中,还有其他办法吗?

答案是肯定的

我们都知道,Javascript中对对象添加属性和方法是非常方便的,例如:

var obj={};

obj.arr = [];
obj.str = "string";

然后又有一句著名的话:

Javascript中,一切都是对象

仔细理解它,很容易想到,函数既然也是对象,那么可不可以直接给函数添加属性和方法呢? 当然可以

function colorPicker(){

    return this;
}

colorPicker.foo = "foo";

console.log(colorPicker.foo)            //foo
console.log(new colorPicker().foo)    //Undefined

如上代码,我们可以把函数当成对象,添加属性和方法.这被称为函数的静态变量和静态方法

需要注意的是,一旦实例化后,实例就在它的原型链上找不到这些静态的东西.new colorPicker().foo返回Undefined就说明了这一点.

静态变量和静态方法只能通过函数名进行访问.这是十分正常的,因为原型链只包括实例与众多Prototype,并没有包含构造器函数,因此肯定无法查找到函数上的这些属性的.

深入理解Javascript中的对象

拾色器这个项目在维护时遇到的如下的场景:

早期版本返回的值是一个RGB的数组.而版本升级后,我想返回更多的属性,例如HEX字符串和HSB数组,但是并不愿意直接将返回值替换为一个对象,因为这无法兼容早期版本,应该怎样做?

还是下面这句话:

Javascript中,一切都是对象

连函数都能直接添加属性和方法了,数组当然也可以!

var arr = [1,2,3];

arr.string = "test string"
arr.getString = function(){
        return arr.string;
    }

console.log(arr.getString());//"test string"

这些给数组添加的属性和方法,同样可以称之为"静态"

直接为对象添加静态属性和方法,被茫茫多的Javascript库所采用,例如博主之前分析的JSVerbalExpressions正则库.这个库利用了VerbalExpression.injectClassMethods方法,来为正则表达式添加静态属性和方法,实现了整个库的功能与逻辑

    function VerbalExpression() {
        var verbalExpression = new RegExp();

        // Add all the class methods
        VerbalExpression.injectClassMethods(verbalExpression);

        // Return the new object.
        return verbalExpression;
    }

    // Define the static methods.
    VerbalExpression.injectClassMethods = function injectClassMethods(verbalExpression) {
        var method;
        // Loop over all the prototype methods
        for (method in VerbalExpression.prototype) {
            // Make sure this is a local method.
            if (VerbalExpression.prototype.hasOwnProperty(method)) {
                // Add the method
                verbalExpression[method] = VerbalExpression.prototype[method];
            }
        }

        return verbalExpression;
    };

为Javascript对象添加静态属性和方法,确实是一个非常优雅的办法

判断类型

在前端工作中,校验输出的类型是必须的,本项目中的相关的代码如下

colorPicker.prototype.isType =  function(context,type){
    return Object.prototype.toString.call(context) == "[object "+type+"]"
}

这实现了一个判断参数一的类型是否是参数二的函数.利用的原理是Object.prototype.toString,再用call或apply将我们的对象替换进去.Object.prototype.toString是一个非常特殊的prototype方法,基本可以说,只有它才能访问得到一个具体对象的类型,这和ECMA3.0标准的[[class]]属性的特殊规定有关

关于"context"的翻译,我认为"运行环境"描述地比较准确,"上下文"也可以理解.博主并不是推荐某种翻译,湾湾把Object叫"组件",也没见到与国内的"对象"这种叫法有什么冲突

结语

本次实践的项目用到的Javascript就是这些.除了Javascript本身,此项目还实现了Adobe Kuler中的色盘算法,最终不用图片而是直接画出了Kuler的色盘,将文件体积减小了95%,如下图:

另外,在算法的实现过程中,博主发现了Adobe Kuler的一个一直存在的BUG:在绿色部分有大约10°的RGB全部相同,很像算法出错导致颜色骤变后再打了模糊.非常有意思.

本文链接:https://smallpath.me/post/Javascript:一次prototype的实践

-- EOF --