困境
  • js的生命周期, 网上文章不多, 仅有的一点文章又大多以讹传讹.
  • 如果要探讨js的生命周期, 那么prototype就无法避开了
  • 而关于prototype这一块, 因为牵扯了太多的解释器实现细节, 因此各个书籍和文章多语焉不详, 甚至多有谬误, 特地写了这边blog, 盼望着js啥时候也有类似jvm指南这样的好书来讲讲实现, 也有bill joy这样的大牛来讲讲v8引擎.
  • 哎, 真心说mdn上面没啥有用的信息, 万能的stackoverflow也貌似失灵了, 这个时候, 谁来拯救我? 竟然依旧是marjin!!!!!!!!

顺便吐槽一句, 数学爱好者面临的问题其实其他人也很容易面临: ‘稍微有点难度的东西, 就会有一大堆人胡说八道, 而真正知道答案的人又不屑于去讲解’
我比胡说八道的看起来要强一丢丢, 但是, 诚心盼望有高人指点迷津

研究思路
  1. 原型的执行时间, 其实我自己log一下应该就知道了.
  2. 对象和原型是相关的, 所以要看一下.
函数

简单的我们回顾一下函数

function factorial(n){ //递归调用需要函数名了. 一般都不这么定义.
  if ((n == 0) || (n == 1))    return 1;
  else    return (n * factorial(n - 1));
}
var square = function(number) { //匿名函数定义
	return number * number; 
}
//函数引用和函数名同时存在的定义
var square = function xxx(n) { //一般都用这个定义.
  if ((n == 0) || (n == 1))   return 1;
  else    return (n * xxx(n - 1));//这里xxx和square都可以
};
//但是上面这个定义, 执行函数的时候只能是square()不能用xxx()了.

//更简洁的匿名函数 =>
var a = [
  "Hydrogen",
  "Helium",
  "Lithium",
  "Beryllium"
];
var a2 = a.map(function(s){ return s.length }); //老的匿名函数的做法.
console.log(a2); // logs [ 8, 6, 7, 9 ]
var a3 = a.map( s => s.length ); //新的胖箭头写法
console.log(a3); // logs [ 8, 6, 7, 9 ]

//5.square, 这样就成功了, 这个递归很有趣.
Number.prototype.s = function x() { 
 const n=this.valueOf();
  if ((n < 2))   return 1;
  else    return (n * (n-1).s());
} ;
(5).s();

//去掉递归, 可以看这个简单的例子.
Number.prototype.r = function() {
    return Math.round(this.valueOf());
};
(5.5).r(); //第一种形式
5..r(); //第二种形式, 不能有小数点了.
5.5["r"](); //第三种当做数组属性处理的形式. 

//针对array更简单了.
var colors = ['red', 'green', 'blue'];
Array.prototype.s = function x() { 
  	this.forEach(function(color) {
 		 console.log(color);
	});
} 
colors.s();
闭包

谈到了函数就不能不谈闭包, 这个是js特色, 为啥用闭包? 其实是为了封闭对象的私有属性.

为啥用闭包, 这个实在是忘记了, 还要再看一下下, 为啥不是简单的使用函数, 而是要用闭包.

  1. 明白了, 闭包拥有可以访问私有属性的方法了.
  2. 闭包返回一个对象更有意义, 一般情况下.
  3. 这个对象可以访问闭包函数拥有的私有属性, 这些属性外部无法访问.
  4. 如果我就是定义一个函数, 用来返回对象, 那么我还要执行这个函数. 来得到他返回的对象, 因此, 这个函数的函数名就毫无意义, 把定义这个函数和执行这个函数合二为一之后, 就是闭包了.
  5. 闭包其实就是直接执行一个函数, 得到返回的对象, 这个对象还有自己的私有属性, 这些属性定在了这个直接执行的闭包函数里面.

示例:

//闭包可以return 一个函数, 但是一般而言, 闭包return一个对象更有用, 即便return一个对象, 也需要一个函数包装起来.
var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
对象

唯一不是函数的就是对象了, 对象不是函数, 但是, 对象是函数的儿子

function f(){};  //函数 
var o = new f();  //对象, 而且是构造函数制造出来的普通对
var o1 = {};  // 直接生命对象, 其实使用Object构造出来的. 等同于: var o1 = new Object(); 
var f2 = function(){};  //函数
猜测
log("f.prototype", f.prototype)  //f {} 函授拥有prototype
log("f2.prototype", f2.prototype) // f2 {}
log("o.prototype", o.prototype)   //undefined 对象没有prototype
log("o1.prototype", o1.prototype)  //undefined
log("o.constructor.prototype", o.constructor.prototype)   //f {}
log("o1.constructor.prototype", o1.constructor.prototype)  //{}

一个猜测: 对象用new创建, 基于构造函数(其实就是一般的函数), 才可以用prototype?

原型
let f=function(){} //定义函数
f.prototype.p=function (){ } //在f的原型上定义了一个p
let b=new f  //此处可以省略圆括号, 定义了一个对象, 等同于 let b = new f()
log("b.constructor ===f :", b.constructor ===f)//true, f是o的构造函数, o是f的实例
log("b.__proto__", b.__proto__) 
log("f.__proto__ === Function.prototype:", f.__proto__ === Function.prototype)//每个对象和函数都有 __proto__ 属性,但只有函数才有 prototype 属性
log("b.__proto__ == f.prototype:", b.__proto__ === f.prototype)  //true, 函数对象的__proto__是他的构造函数的prototype
log("f.prototype.constructor===f", f.prototype.constructor===f)  //true, 函数对象的原型的构造函数就是函数自己, //实际上, 原型就一个构造函数的实例, 这个实例本身也是一个可以有prototype属性的函数对象

log("b.__proto__===b.constructor.prototype", b.__proto__===b.constructor.prototype)  //这里是true, 对象的__proto__就是他的构造函数的prototype


//---------------------------------神奇的分割线-------------------------------------
//神奇的事情发生了. prototype的__proto__的定义是个特例
log("f.prototype.__proto__===f.prototype",f.prototype.__proto__===f.prototype) //这里是false, prototype的__proto__并不是他的构造函数的prototype
let ppp=f.prototype
log("ppp.__proto__===ppp.constructor.prototype", ppp.__proto__===ppp.constructor.prototype) //即便花了写法, 这里也是false
log("f.prototype.__proto__===f.prototype.constructor.prototype",f.prototype.__proto__===f.prototype.constructor.prototype)  //false, prototype的__proto__定义并不是正常的定义.
原型链
var obj = {} //它等同于:var obj = new Object(), 都是语法糖, 字面量明显更清晰, 在js中字面量才是王道.
//-------------------------------字面量才是王道-------------------------------
log("obj.constructor === Object",obj.constructor === Object)// false, f.prototype的构造函数并不是Object.
log("obj.__proto__ === Object.prototype",obj.__proto__ === Object.prototype)// false, f.prototype的构造函数并不是Object.

//-------------------------------原型链硬性组织到了object上面-------------------------------
let f=function(){} //定义函数

log("f.prototype.__proto__ === Object.prototype",f.prototype.__proto__ === Object.prototype)// true, 很神奇prototype的__proto__统一都是Object的prototype

log("Object.__proto__===Function.prototype",Object.__proto__===Function.prototype) //true, 一切都是函数或者函数的儿子.
log("Object.constructor == Function ",Object.constructor == Function)//true, 一切都是函数, 或者函数的儿子.
///不仅仅Object是这个表现, Number数字, boolean布尔, string字符串, function函数自己, array数组, regexp正则, error错误, date时间, 都是这个定义.
///例外的是 math和json的构造函数是Object, 也就是function的孙子
//简单地说所有的constructor的__proto___都是function.prototype, 注意这个是说所有的.

log("Function.prototype.__proto__ === Object.prototype", Function.prototype.__proto__ === Object.prototype) // true, 明白了吧所有非function的prototype都硬性规定为Object.prototype
log("Object.prototype.__proto__ === null ",Object.prototype.__proto__ === null )// true)根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
console.log("Function.prototype.prototype", Function.prototype.prototype) //undefined, function的prototype虽然是function, 但是, 硬性规定了他没有prototype.
//明白了吧, 所以function.prototype.__proto__无论如何也得是Object了.//除了function之外, 其他的类型的prototype已经都是object了
一切都是函数

函数才是js的第一公民

  1. 函数是js的第一公民, 函数可以当做值来处理, 函数可以作为参数, 也可以作为返回值, 可以出现在赋值等号的右边. 函授还是js中唯一拥有prototype的.
  2. 就像如果一切都是对象那么语法会很繁琐一样, 一切都是函数最终也会导致繁琐. 所以js引入了很多语法糖, 数组, 对象, 正则, map, 数字(Number), 字符串(String)…… 这一切都仅仅是函数的语法糖而已.
  3. (构造)函数的prototype就是函数的原型, 是所有同一个构造函数构建出来的对象的共性,
  4. 下面讲一下prototype的硬设定:
    1. (函数f的prototype)的构造函数就是函数f本身: f.prototype.constructor===f //true 参见前一段的代码
    2. prototype的构造函数的原型就当做Object的原型 //由此推断: f.prototype.constructor===Object //false. 这个必须是false, 不然和上一个就矛盾了.
  5. 为啥会有硬设定, 下面是我的猜测, 但是估计八九不离十
    1. 循环定义再数学上是完美而优雅的, 比如二叉树的定义: 二叉树有两个叉, 这两个叉要么是二叉树要么是空.
    2. 但是, 再程序设计和解析上是费劲的, 比如C++就面临元语言的各种问题, lisp系的语言之前也是解析出来运行效率差. 包括java, java的虚拟机很牛, 但是java的编译器令人发指, 所以才有了那么多jvm语言, 但是, 大家发现很难再效率和优雅之间做平衡.
  6. Object的构造函数就是function了, 一切都是function………
  7. 到这里我们发现function自己构成了一个死循环, 一切都可以归结为function, 但是, 如果真的这么实现, 估计编译器会受不了, 所以, 才有了上面的硬性规定, prototype统一归结为Object的实例, Object的的起源为null, 这样编译器写起来就开心了.

总之, prototype里面不优雅的部分纯粹是一个实现意义上的妥协. 真正优雅的定义就再function里面自循环定义就好了. 然后, 我们研究了这么多细节, 其实离开function之外的部分(就是硬定义prototype« Object « null这个链)其实没啥意义. 不需要记忆也不需要理解(因为硬定义的, 其实也没啥可理解的).

纠正一句话:
  • 函数也是第一公民, 这个错了, 而且错的离谱.
  • js之中, 函数是唯一的第一公民, 这样描述才是正确的. 才能解释我们再语言使用中遇到的问题.
  • 他自身形成了循环定义, 但是, 为了编译器的实现, 才引入了Object. Object只是实现方案, 并不是js这门语言的设计方案.
  • 同样为了书写简便, 引入了各种字面量, 因此字面量是语法糖,
  • 然后大家使用字面量的时候就尽量直接使用字面量, 不要使用对象去定义字面量. 用对象去定义字面量, 就是用一个妥协的方案去搞定一个为了简化书写的方案, 想想就知道里面一定会出各种坑爹的事了.
再次总结一下js的简化编译方案

为了简化实现:

  • 所有的constructor(构造函数, 包括前面说的object, number, array, map, string, exrep…… js中所有的数据类型+函数+对象)的____proto____都指向function.prototype, 注意这个是说所有的, 这个是正常的一切都是函数, 但是, 这么玩, 到了prototype的层次就形成死循环了, 所以有了下面两条硬定义.
  • 所有的prototype.____proto____都指向Object.prototype
  • Object.prototype.____proto__ === null
简化方案引发的各种诡异
//再次明白一点
let b=new f  //此处可以省略圆括号, 定义了一个对象, 等同于 let b = new f()
log("b.__proto__===f.prototype",b.__proto__===f.prototype)// .
log("b.__proto__===b.constructor.prototype",b.__proto__===b.constructor.prototype)// .
log("b.constructor===f",b.constructor===f)// .


//把f的原型换成对象字面量, 我们知道, 他的原型本省应该也是一个函数的, 我们硬换成对象
f.prototype = {}   //其实这么搞已经错乱了, 按照本身的定义应该不允许这么搞, 但是, 因为有那个原型链的兑付的补丁方案, 就是  object->null, 所以可以这么干. prototype被硬性规定为普通Object实例
let bb=new f  //此处可以省略圆括号, 定义了一个对象, 等同于 let b = new f()
log("bb.__proto__===f.prototype",bb.__proto__===f.prototype)// .
log("bb.__proto__===bb.constructor.prototype",bb.__proto__===bb.constructor.prototype)//这里是一个false
log("bb.constructor===f",bb.constructor===f)//这里又是一个false, 看到了吗? bb已经不承认f是构造函数了.
log("bb.constructor===Object",bb.constructor===Object)//true, bb已经是Object的实例了, 实际上bb和那个构造函数f没啥关系了.

f.prototype = {nnn: "xxxxx"}   //
let bbb=new f  //此处可以省略圆
log("bbb.nnn",bbb.nnn)  // xxxx是可以拿到的, 这时候f已经不是一个函数了, 他就是个Object了. 简单地说, f因为被硬赋值了一个对象, 导致f变成了对象.
log("typeof(f)",typeof(f)) //这里显示f依旧是函数, 分裂了, 因为这个临时的补丁方案, 导致f分裂了.

简单的说, bb和f.prototype有关系(这个prototype实际是Object的), bb和f没关系, 因为f是全局定义的函数
本来f还是bb的constructor, 但是, 由于f的prototype直接object了, 所以这个关系也不成立了.

反思一下

之所以prototype比较难理解是因为我们的理解和学习顺序反了, 应该先理解____proto__ 这个就顺了.

  • ____proto____是定义你的那个本源(妈妈).
  • 一切的____proto____都是function.
  • 只有function需要prototype, 因为, function需要在一个地方给大家定义这个本源(妈妈).

原型链指的是: ____proto__, 其实原型链和prototype没关系…….

实用
function isArray(myArray) { //判断myArray是否一个数组, 通过看他的constructor
    return myArray.constructor.toString().indexOf("Array") > -1;
}
那么回到之前blog探讨过的memory技术:
var log = console.log
function be(){}
var lim= 100
be.prototype.getL = function(x) {
    let length=1
    for(let i=1; i<lim; i++){ //为了慢一点的计算, 但是阶乘还是很快的, 要考虑换成其他计算才好.
        length *= i
        if(i===1)log(x, "run")
    }
     this.getL = function(){return length};  //奶奶的并不是运行期的问题, 而是把prototype的getL属性重新定义了.
    return length;
    // expensive computation
    //return (this.getL = function(){return length})();
}

let xx = new be
log("xx1:", xx.getL("xx1")) //第一次计算, 对prototype.getL进行了重定向
let xxx = new be
log("xxx", xxx.getL("xxx")) //重新计算了, 因为再次调用了原型属性
log("xx2", xx.getL("xx2"))
log("xx3", xx.getL("xx3"))
log("xx4", xx.getL("xx4"))
总结
  • js的生命周期真心简单, 基于函数的, 函数执行时有生命, 函数结束生命终止.
  • 例外就是关于引用, 从一个根部进行树状引用, 遍历不到的元素会被垃圾回收. 就是这么简单.
//纯炫技, 一句话的写法确实比较炫
be.prototype.getL = function(x) {
    let length=1
    return (this.getL = function(){return length})() //虽然酷炫, 但是, 其实多了一次函数调用, 本来赋值, 然后返回length就好了, 这样是赋值, 并且执行一次才拿到length.
}
最终的告白

维特根斯坦说: 看得懂我说的人都明白, 这些话是无意义的, 整本书证明了这个主题的无意义.

这篇blog也是这样的, 通篇的技术都会改变正常的js书写, 都属于frawework范畴而不是lib范畴, framework和lib的区别就在于是否改变语法, 这一篇的几乎所有技术都会改变语法. 因此, 严重不推荐使用, 至少请谨慎使用.

那么, 这篇blog的意义何在? 道可道非常道, 名可名非恒名, 如梦如幻, 如露如电, 一切如是观, 一部论证这个世界无意义的书, 本身也是有很大意义的.

在数学领域, 哥德尔证明完备和自洽不可兼得, 图灵证明停机不可自判断. 这些都是有意义的.

这篇blog的意义在于, 由于js是妥协的产物, 因此, 我们不宜太过使用它的functional本质特性, 当然, 我们更不该过分使用它的object妥协特性, 这两个特性的充分使用都会导向framework, 这种恶果再其他语言已经有了体现, 比如java的每一个framework都等同于一种新的语言.

另外还有一个附加的意义, 当我们面对大神级代码的时候, 可以阅读, 虽然很多东西都说好了不用的, 但是, 基于实际情况大神是可以用的, 比如bill joy 的一行内存调度, nginx的四星(指针)级代码. 比如之前我就看不懂marjin haverbeke的代码, 不论是codemirror还是prosemirror. 写了这篇blog之后, 我可以继续去尝试阅读他的大作, 顺便说一句她是object的支持者.

顺便说一句, 目前已知的语言之中, 可以认为js的地址指针机制是最好的, 使用难度和优雅程度做了很不错的平衡, 各种字面量的设计也很不错.

参考:

  • https://www.jianshu.com/p/dee9f8b14771
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
  • https://marijnhaverbeke.nl
后记:

序列化

var A = function () { 
    this.objectsOwnProperties = "are serialized";
};
A.prototype.prototypeProperties = "are NOT serialized";
var instance = new A();
console.log(instance.prototypeProperties); // "are NOT serialized"
console.log(JSON.stringify(instance)); 
// {"objectsOwnProperties":"are serialized"} 

作用域和this以及eval

参考: https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work

scope

http://www.digital-web.com/articles/scope_in_javascript/

寻寻觅觅有用的

一篇正经的探讨这个问题的文章: https://www.thoughtco.com/javascript-execution-order-2037518

js有三个地方可以执行

  1. 页面的头部head
  2. 页面的body
  3. event handler

这三个地方是直接编码, 还是引用进来并不重要.

  • 1, 2这种情况, 只要到了就开始执行. 因此如果要操作dom, 你的code至少要在dom之后的位置才行.
  • 在function或者object声明这种, 调用就会执行.
  • 作为事件的handler或者回调执行, 不需要(), 所以是当事件/回调发生的时候再执行.
  • 浏览器环境本身还有一堆的执行顺序, 比如浏览器的js, 客户的js(至少safari是可以指定客户css/脚本的)
浏览器处理周期

https://stackoverflow.com/questions/44044956/how-does-browser-page-lifecycle-sequence-work

  1. Construction of Document Object Model(DOM) 阻断性的, 而且比较耗时
  2. Construction of CSS object model(CSSOM)
  3. Construction of Render Tree 合并上面着两棵树
  4. Layout 布局, 计算到像素
  5. Paint. 把像素画到屏幕
  • 那么script咋办? 答案很惊人, script会阻断DOM, 碰到script就会阻断dom, 执行script, 不论是直接写入还是引入. 因此最好异步加载, 异步调用(DOMContentLoaded)
  • cookies
匿名函数
  • 据说匿名函数=>每次调用都会生成一个函数实例, 这个明显不对啊, js的编译器得傻成啥样才会这样? 随便编码一下不就好了吗? 不至于每次都生成一个实例啊.
  • 不管你们信不信, 反正我是不信的.
react 生命周期
  • 官方还真的有认真的介绍这个: https://reactjs.org/docs/state-and-lifecycle.html