The Core of JS

  我基本從來不寫工作的事兒。

  因為工作實在沒啥好寫的,不就是工作唄。

  然後今天打算稍微寫一點,就寫JS吧。


  我一直相信,所有的編程語言都可以分為兩部分,表象與內核。

  什麼是表象?語法就是表象。

  什麼是內核?去除表象以後的就是內核。

  很多人學編程語言學的就是語法,但語法這貨只要記憶力足夠OK,半天就能搞定嘛。

  我司有一位新人,現在已經被開除了,他說過這麼一句名言:

  我會編程啊,C、C++、Java、Ruby和Python的For循環或者If語句我都會的。

  一旁旁聽的我聽了只能笑笑。

  這貨另一句名言是:這個和我學校裡學的不一樣啊!

  大家一起呵呵。

  PS:此人交大畢業,到我司的筆試聽說是滿分。

  回到正題。

  語法是表象,不重要——當然了,內核總會牽扯到語法怎麼實現的,所以並不是說那麼地不重要。

  關鍵是拋開表象後的東西。

  在我看來,JS在拋開無聊的語法表象後,真正剩下的東西是這些:


  1,對象與域

  對象,Object。

  域,Scope。

  一切變量要么隸屬於某個對象,要么隸屬於某個域——最外層的域當然就是DOM/BOM了。

  比如下面這個:

var name = '???';

function classA () {

    var tag = '~~~';

    this.name = 'test';

    this.show = function () {console.log(this.name + tag + name);};

}

var test = new classA();

  這裡,最外層的對象有兩個,name和test(String也是對象)。而最外層的Scope是DOM/BOM,然後是test內的Scope。

  所以以var形式生命的對象都隸屬於其所在的Scope,而所有以this.xxx形式聲明的對象都隸屬於所屬的對象(比如classA中的this.name,在實例化後name這個對象就隸屬於test這個類實例)。

  Scope的最大特點,就是Scope是可以傳遞的,這也就是JS中的callback這麼強大的一個原因:

var name = 'xxx';

body.addEventListener('click', function clickHandler (evt) {

    var tag = 'aaa';

    $.ajax({...})

    .done(function ajaxHandler  (result) {

        console.log('Done', name, tag);

    });

});

  在上面的例子中,最外層的scope被傳遞給函數clickHandler,或者更應該說是最外層scope的訪問權限被傳遞給了clickHandler的scope,於是你可以在clickHandler內訪問外層定義的變量name。

  同樣的,clickHandler的scope和最外層scope被一起傳遞給ajax請求的處理函數ajaxHandler中,浴室你在這裡面也就可以訪問最外層的name和clickHandler中定義的tag了。

  如果你看過V8(Google給Chrome系用的JS引擎)的源碼,你就能找到一個Scope對象,就是用來幹這事的。为V8写插件或者给NodeJS写调用V8的插件(这种一般都是C/C++写的了)时,一定要记住给予恰当的Scope,将一个底层对象放到不同的Scope中往往会得到不同的效果,因为可以访问的东西不一样了。

  当然不是只有Scope可以传递,Object也可以传递,这就牵扯到了JS中的原型链,等下再说。

  对于scope来说,最合理的理解是这样的:

  一个对象可以隶属于多个scope。如果它隶属于多个scope,则所有scope必然满足这样一种链型关系:S1∈...∈Sn。也就是说,Sn必然是最大的scope,然后Sn中包含有Sn-1,Sn-1包含Sn-2,以此类推,知道最小的S1,这里就是某个对象所在的最小Scope。

  而后,一个对象只能访问同Scope的变量,或者说只有同属一个scope的对象才能相互访问,这就是Scope内变量的访问规则。

  当一个对象隶属于多个Scope时,它访问别的对象的顺序就是上面的那条链,它会先在S1中寻找指定对象是否存在,然后去S2中寻找指定对象是否存在,一直到找到指定名字的对象位置——所以说对指定名字的对象的搜索是会在链条的某处截断的,而不会每次都遍历一遍整个链条。这也就是说,包含对象的更小的Scope会“覆盖”掉更大的Scope中的同名对象。

  JS中的所有一切都是对象,唯独scope不是,这是因为这货是JS引擎提供的“编制外”成员,并不是JS可以直接访问和操纵的东西——所以,我们可以用原型链来修改一个对象的父对象的方法,从而实现元编程,但如果一个对象的方法是用了类定义函数scope中的“私有”函数的话,我们就对它无能为力了,因为无法操纵scope。

  JS最“神奇”的功能之一“闭包”也就是利用scope的特性——闭包不能被外界访问,这是因为scope内对象不能被外界访问,而只能被scope内对象访问。

  如果我们在Chrome中打开一个页面,那么可以在console中查看一个对象所属的closjure,这是新版本Chrome所提供的功能。

  Scope不能被用户在JS中有意识地创建,而只能在创建某些JS对象的时候“附带”地获得。

  比如在上面的例子中,我们创建一个function,那么就获得了这个function内的scope。我们new了一个类实例,也就在这个实力内创建了一个scope——当然,这两件事其实是一件事。

  又由于scope内的对象只能被scope内对象访问而不能被外部访问,所以我们就有了JS中的public和private对象的创建方法,那就是上头第一段代码中的classA这个类。

  这个类被实例化后,就有了一个Object和一个Scope。在Scope中我们存放了一个对象tag,而在Object内我们存放了两个对象name和show。外界可以通过test.name来访问test这个Object的对象,但无法访问test被创建时所处scope内的对象tag,从而name和show对外界来说是public的,而tag对外界来说是private的。

  这是scope的一个比较重要的用法。

  我们可以比较一下JS和Ruby中Scope的差异。

  Ruby中的Scope在Class、Module和Def中都是闭合掉的,比如:

my_var = "Global"

class ClassTest

    my_var = "Inside Class"

    name = "Test-Class"

    def show

        puts "My Name Is #{name} in #{my_var}"

    end

end

test = ClassTest.new

test.show

  这样,最后会返回错误,因为在show内无法访问my_var,也无法访问name。

  这是和JS中最大的不同。要穿越这个闭合的Scope Gate,可以用如何方法:

my_var = "Global"

ClassTestA = Class.new do

    name = "Test-Class"

    define_method :show do

        puts "My Name Is #{name} in #{my_var}"

    end

end

test = ClassTest.new

test.show

  这样就可以访问class外定义的my_var变量了。

  可这么做也有缺点,那就是show这个函数无法被重载,一旦被重载,重载的地方将无法访问name,甚至无法访问my_var。

  回到JS中。JS的Scope的传递方式前面已经介绍过了,所以如果我希望在ScopeA里访问非ScopeA的上层的ScopeB中的变量,原则上是不可能的,比如下面这个:

var tag = "global";

function show_tag () {

  console.log(tag);

}

function test (callback) {

  var tag = "inside";

  callback();

}

test(show_tag);

  上面的运行结果是global。如果我们希望show_tag访问test内的tag,这个一般是不可能的,但凡事总有例外,于是就有了这么一种很危险的突破方法:

function test (callback) {

  var tag = "inside";

  eval("var __callback__ = " + callback.toString() + ";__callback__();");

}

  这样的运行结果就会是我们所期望的inside。

  这种方法的原理其实是是用eval来动态构造函数并执行该函数。当我们使用eval来动态构造函数的时候,因为执行的是新的函数,所以原函数的scope完全无效,新函数的scope完全由构造函数的scope决定。

  当然,因为使用eval来动态构造函数,所以这个方法本身也会带来很多风险。


  2,原型链

  先看这么一段东西:

function classA (name) {

    this.name = "I'm " + name;

    this.show = function () {

        console.log('>>  ' + this.name);    

    }

}

var a = new classA('A'), b = new classB('B');

a.show();

b.show();

console.log(a.__proto__ === b.__proto__);

console.log(a.show === b.show);

console.log(a.__proto__.show)

  我们看到,a和b的原型(或者说父对象)是相同的,但a和b的show方法是不同的,而且a和b的父对象是没有show方法的。

  接下来这么做:

a.__proto__.test = function () {

    console.log('Test    ' + this.name);

};

a.test();

b.test();

console.log(a.test === b.test);

console.log(b.__proto__.test);

  这么一来,事情就好玩了,我们终于在a和b的原型上添加了一下好玩的东西了。

  事实上,new一个类的过程其实是这样的:

function classA () {

    var a = 'A';

    var b = 'B';

    this.x = 'X';

    this.y = 'Y';

}

var testA = new classA();

function classB () {

    var a = 'A';

    var b = 'B';

    var obj = {};

    obj.x = 'X';

    obj.y = 'Y';

    return obj;

}

var testB = classB();

  我们用new classA()创造出来的东西,和用classB()创造出来的东西,其实是一样的。

  这样,回到一开始的问题:为何a.show和b.show是两个不同的函数?

  原因很简单,它们是两个不同的对象a和b所独有的函数,而不是来自于某个特殊的共同祖先——原型。

  可是,有的时候a和b的test函数其实做的是完全相同的事情:将对象的name属性打印出来。既然如此,如果这函数不是公用的,这就表示我有几个classA的实例,就创建了几个show函数,这对内存是极大的浪费,从而为了节约内存,我们需要这样:

function classA (name) {

    this.name = name;

}

classA.prototype.show = function () {

    console.log("My Name Is " + this.name);

};

  这样就好了。

  其实这个做法等价于:

(new classA()).__proto__.show = function () {

    console.log("My Name Is " + this.name)

};

  这就引出了很好玩的关于原型链的话题。

  JS中的对象通过原型链来实现“继承”,而原型就是上述obj.__proto__这东西。

  通过一个对象的__proto__接口,我们可以方位这个对象的“原型”。

  要理解原型,先要理解类和实例。

  从上面的分析可以看出,类可以被理解为构造对象的一个函数,因此JS中其实只有对象的概念,没有类的概念,虽然我们可以通过instanceof来判断一个对象是否是一个“类”的实力,但实际上这货所作的是比较一个对象的constructor属性所指的函数是否是这个对象的构造函数。

  因此,JS中其实没有类和实例,有的只是构造函数和对象。

  对象的原型是另一个对象,几个对象C1、C2、C3可以共有一个原型对象P。当一个对象要寻找对象属性或者方法(而非Scope属性或者方法)的时候,会先从自己开始找起,自己没有的话就去原型那找,原型也没有的话就去原型的原型那找,以此类推。

  原型只能传递或者说提供“钩”在对象上的属性和方法,而不能提供Scope。

  而这一套机制,就被称为原型链。

  所以说JS的“继承”和别的OOP的继承是那么地不同,因为原则上说,JS根本没有类,所以也就谈不上继承了。而原型链又在表现上有点类似继承的关系,所以就让人以为JS也有“继承”。

  回到一开始的问题,如果我要让show方法不被创建多次,应该怎么做?方法就是使用构造函数的prototype来给出方法。

  考虑上prototype后,其实一个构造函数所作的是这么一件事:

function classA () {

    this.A = 'A';

}

classA.prototype.show = function () {

    return this.A;

};


var classB_prototype = {};

classB_prototype.show = function () {

    return this.A;

};

function classB () {

    var obj = {};

    obj.A = 'A';

    obj.__proto__ = classB_prototype;

    return obj;

}

  这么一来,我们应该就可以看清楚原型和构造函数到底是干嘛的了。

  于此相关的,就是对象的this这个属性了。

  这是JS引擎提供给对象的固有属性,可以认为是一个指针,决定了指向具体什么对象——到底是指向构造函数创造出来的对象,还是这个对象的原型,还是整条原型链上的某个环节。

  需要注意的,就是this本身是属于scope的,this.xxx才是属于this所指向的对象的,所以在很多时候直接使用this会出各种各样的幺蛾子——特别是在使用匿名函数回调等等东东的时候,一个scope切换,就等着乐呵吧。

  this的指向是可以被修改的,这就是JS里经常看到的xxx.call方法,将一个函数xxx内的this指向到指定的obj——

function ClassA (name) {

    this.name = name;

}

ClassA.prototype.class = "Class A";

ClassA.prototype.show = function () {

    console.log("Object " + this.name + " Of Class " + this.class);

};

var objA = new ClassA("Object A");

objA.show();

var objTmp = {

    name : "No Name",

    class: "No Class"

};

objA.show.call(objTmp);

  运行结果为:

Object Object A Of Class Class A

Object No Name Of Class No Class

  由于在复杂环境中,Scope的转移和this的转移根据不同的规则来——scope依赖于上下文context,而this依赖于原型链,所以JS才会有很多很丰富的特性——尤其是各种回调和异步方式。

  我们可以ruby做一个对比。

  在ruby中是的确有类这个概念的,而且,ruby的动态特性允许我们修改已经定义好的类的定义——这在js中是做不到的,我们只能修改一个对象的原型,但不能修改这个对象的类定义本身——所以JS的诸多编程模式中有一个就是在所有能用原型的地方都用到了原型,这样当我要修改类定义的时候,就把原型充当类定义来用了。这么做有好的地方,当然也有不好的地方——原型链过长的话,每次用this调用属性/方法的时候的开销也就大了。

  而另一方面,ruby可以通过module和mix-in做到很好的代码重用和多重继承,这个在js里比较麻烦,因为一个js的原型只有一个——当然,这事也不是说就做不到,只不过麻烦一点:

function clsP1 () {

    this.funcitonA = ...

}

function clsP2 () {

    this.funcitonB = ....

}

funciton clsPMain () {

    this.propertyC = ....

}

function clsChild () {

    var me = this;

    var p1 = new clsP1(), p2 = new clsP2();

    this.functionA = function () {

        p1.functionA.call(me, arguments);

    };

    this.functionB = function () {

        p2.functionB.call(me, arguments);

    };

}

clsChild.prototype = new clsPMain();

  在上面的例子中,clsChild的实例的原型是clsPMain的实例,但clsChild的实例依然可以使用clsP1和clsP2的方法(而且this指向的确是它们俩)。这也是一种很常见的多重继承方案。

  与此相关的一个有趣的现象,那就是JS中的对象是可以“换家长”的,而ruby就不能这么“放肆”:

var ParentA = {

  show: function () {

    console.log(">>>>>> " + this.name);

  }

}

var ParentB = {

  show: function () {

    console.log(this.name + " <<<<<<");

  }

}

function classClass (name) {

  this.name = name;

}

classClass.prototype = ParentA;

var obj = new classClass("Lost");

obj.show();

classClass.prototype = ParentB;

obj.show();

obj.__proto__ = ParentB;

obj.show();

obj.__proto__ = ParentA;

obj.show();

  像这种随便换老爹的事情,大概是JS绝无仅有的吧。。。这也是原型链和继承的最大不同。

  我们事实上可以说,所谓继承,就是类的原型链,但和对象的原型链毕竟还是不同的。

  Ruby虽然一样是动态语言,但Ruby毕竟还有类的概念,从而也有访问器的概念,而这个JS就没有。所以当我们在看Ruby很欢脱地private、public或者protected的时候,JS的心理总是会有一些酸楚。当然,世事无绝对,JS也可以玩访问器,不过就是麻烦点:

function isNull (obj) {

  if (obj === null) return true;

  if (obj === undefined) return true;

  if (typeof obj === 'undefined') return true;

  return false;

}

function classMap () {

  var keys = [], values = [];

  this.set = function (key, value) {

    var index = keys.indexOf(key);

    if (index === -1) {

      keys.push(key);

      values.push(value);

    }

    else {

      values[index] = value;

    }

  };

  this.get = function (key) {

    var index = keys.indexOf(key);

    if (index === -1) return null;

    return values[index];

  };

  this.remove = function (key) {

    var index = keys.indexOf(key);

    if (index === -1) return;

    keys.splice(index, 1, 0);

    values.splice(index, 1, 0);

  };

  this.find = function (value) {

    var index = values.indexOf(value);

    if (index === -1) return null;

    return keys[index];

  };

  this.each = function (callback) {

    var l = keys.length, i;

    for (i = 0; i < l; i++) {

      callback(keys[i], values[i]);

    }

  };

}

var jlass = (function jlass (global) {

  var obj_net = new classMap();

  function clsAccessor (host) {

    this.public = function (name, obj) {

      if (isNull(obj)) {

        return getPublic(host, name);

      }

      else {

        return setPublic(host, name, obj);

      }

    }

    this.private = function (name, obj) {

      if (isNull(obj)) {

        return getPrivate(host, name);

      }

      else {

        return setPrivate(host, name, obj);

      }

    }

  }

  function newAccessor (host) {

    var accessor = new clsAccessor(host);

    checkLink(host).accessor = accessor;

    return accessor;

  }

  function getAccessor (host) {

    return checkLink(host).accessor;

  }

  function checkLink (obj) {

    var link = obj_net.get(obj);

    if (isNull(link)) {

      link = {

        parent: [],

        accessor: null,

        vPrivate: {}

      };

      obj_net.set(obj, link);

    }

    return link;

  }

  function setParent (child, parent, isPrototype) {

    var link = checkLink(child);

    if (!isNull(isPrototype) || link.parent.length <= 0) {

      child.__proto__ = parent;

    }

    if (link.parent.indexOf(parent) < 0) {

      link.parent.push(parent);

    }

  }

  function getPublic (host, name) {

    return host[name];

  }

  function getPrivate (host, name) {

    var vars = checkLink(host);

    var result = vars.vPrivate[name];

    if (!isNull(result)) return result;

    var parents = vars.parent, l = parents.length, i;

    for (i = l - 1; i >= 0; i--) {

      result = getPrivate(parents[i], name);

      if (!isNull(result)) return result;

    }

    return null;

  }

  function setPublic (host, name, obj) {

    host[name] = obj;

    return obj;

  }

  function setPrivate (host, name, obj) {

    var vars = checkLink(host);

    vars.vPrivate[name] = obj;

    return obj;

  }

  var jlass = function (className, structure) {

    var constructor = function () {

      var obj = {};

      obj.extend = function (parent) {

        setParent(obj, parent);

      };

      var args = [], l = arguments.length, i;

      newAccessor(obj);

      args.push(getAccessor);

      for (i = 0; i < l; i++) args.push(arguments[i]);

      structure.apply(obj, args);

      return obj;

    };

    constructor.className = className;

    return constructor;

  };

  return jlass;

}) ();

var classTest = new jlass('Class Test', function (accessor, name, age, sex) {

  this.family = "Winter";

  accessor(this).public('name', name);

  accessor(this).private('age', age);

  accessor(this).private('sex', sex);

  this.sayName = function () {

    console.log("Name: " + accessor(this).public('name'));

  };

  this.sayAge = function () {

    console.log("Age : " + accessor(this).private('age'));

  };

  this.saySex = function () {

    console.log("Sex : " + accessor(this).private('sex'));

  };

});

var objTest = new classTest('Test Object', 18, true);

console.log(objTest);

objTest.sayName();

objTest.sayAge();

objTest.saySex();

console.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');

var classChild = new jlass('Class Child', function (accessor, name, job, age) {

  this.family = "Winter";

  accessor(this).public('name', name);

  accessor(this).private('job', job);

  accessor(this).private('age', age);

  this.sayJob = function () {

    console.log("Job : " + accessor(this).private('job'));

  };

});

var objChild = new classChild("Test Child", "RD", 20);

objChild.extend(objTest);

console.log(objChild);

objChild.sayName();

objChild.sayJob();

objChild.sayAge();

objChild.saySex();

  耶~~


  除此以外的JS相关的部分,个人都认为没啥特别值得记录的,无非就是熟能生巧耳,所以就只写这两个东西。


  临睡前写的总结,万一哪里写错了的话,大家帮忙一起捉虫吧~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,715评论 2 17
  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,050评论 1 10
  • ECMAScript关键字 delete do else finally function in instance...
    doudou2阅读 705评论 0 0
  • 1.语言基础2.严格模式3.js组成(ECMAScript DOM BOM)4.各种(DOM BOM)例子5.组件...
    蒲公英_前端开发者阅读 1,540评论 0 3
  • 在开发iOS的过程中,我们有时候可能需要自己开发一些API给别人使用,但是又不想暴露具体的实现代码给别人(或者防止...
    live111阅读 471评论 0 0