Link: https://informationalmind.appspot.com/2012/04/19/SummarizeJavaScriptUsingShortStatements.html
从春节后开始自学JavaScript,看的JavaScript: The Definitive Guide, 6th Edition这本书,又是断断续续看了2个月左右……看完好久了,一直也没写点什么,过了这么长时间之后,决定还是写一写吧。不系统化的写了,到处都是各种教程,没意义,我就写点我意识到的一些js的特性/注意点/易错点等等吧。仅限于浏览器端的js,这里不涉及到其他平台的js。基本上全是短句,我在想是不是再打上数字序号?呵呵。
看js很大程度上是想搞搞WebGL,所以只是选择性的看了一些相关的部分,CSS之类的就没有看。在没学js之前胡乱写过一个巨垃圾无比的WebGL小程序,写得很恐怖……过一阵子可以考虑写个WebGLRenderer玩玩了,嗯……
下面正式开始~
杂
虽然行末没有分号会被自动添加,但为了安全全部都要写出来。由于这个原因,左大括号是写在行末还是新起一行这个风格问题就只剩下一个方案了:不要换行写。否则会被自动加分号,比如想象一下return {someKey: someValue};这个,如果左大括号写在下一行的话,就成return; {someKey: someValue};了。
基础类型boolean,number(floating point/integer),string是不可变类型(immutable)。对象类型都是引用类型,是可改变的。
有个null,还有个undefined,两者有些类似。undefined简单的说,可以理解为连个null都没有。null是有这个值,值为空,undefined是就没有这个值。
对象本质上来说就是个hash table实现的map。只不过key必须为string类型。
function是一等公民。也是个对象。
各种类型之间的运算充满了隐式类型转换。特别要对基础类型如boolean,string,number,null,undefined之间的混合表达式加以注意。还有对象到string。
在非严格模式下,赋值给未定义变量实际上是赋值给了全局对象上的变量。strict mode下会异常。
大括号不会创建新的scope,只有function定义才会创建scope,这是个和其他语言很不一样的地方。可以用内嵌的function定义并马上调用来模拟scope。
所有的函数都是值传递,无法得到在cpp中void swap(T& a, T& b) {T t = a; a = b; b = t;}这种函数的效果。当然,这个传“值”是引用的值,而不是被引用的对象。这个和C#/Java等语言是一样的(当然,C#有ref/out参数来达到引用参数的效果)。
function也可以用来模拟namespace,对象也是。但这两者的效果很不一样,对象更接近传统意义上的namespace,function中定义的变量对外是不可见的,可以获得private namespace的效果(要自行返回需要的变量)。
function可以捕获其所定义之处所有的变量。也就是说function是closure。(具体见后面scope chain一节)
var定义在scope中的任何地方都是合法的,都会被当作在scope一开始处进行的定义(但和定义一同的赋值还在原处)。
o是一个对象,则o.prop和o["prop"]有相同的效果,但前者prop必须是合法的token才行,后者可以是任意字符串(不是字符串会自动转化为字符串)。
用构造函数创建对象的时候不要忘记new。直接当作普通函数去调用构造函数语义就不对了。(具体见后面prototype chain一节)
任何变量表达式都可以作为判断条件。任何变量都有truthy/falsy的效果。null,undefined,0,-0,NaN,"",这些是falsy value,其他的都是truthy value。
由于没有严格意义上的整形,也就没有类似强类型语言中的向下取整的整数除法,如5/4会得到1.25而不是1;但余数运算还有,而且可以对非整数使用,如4%2.5为1.5。需要向下取整可以用Math.floor函数。
||和&&操作符的返回值和通常的静态语言并不相同,其返回的不是boolean值,而是操作数本身。短路效果根据左右操作数的truthy/falsy判定,规则和其他类c语言相同。所以类似于somevar = a != null ? a : defaultValue;可以方便的写成somevar = a || defaultValue;。
==和===(!=和!==),操作数类型不同的时候,前者会进行类型转换。后者对于对象类型来说,就是引用地址的比较。后者的语义类似于C#/Java之类的相等运算符(不等运算符)的语义。除非有明确的目的,否则避免使用前者。
in操作符可用来判断一个属性是否在一个对象中,delete可以删除指定的属性。如果不用in,对象中某key的value为undefined还是就没有这个key的是无法从语法层面区分的(但可以用Object.hasOwnProperty())。
for (key in object){}循环中,key每次得到的是object中的key,而不是value。
Object.defineProperty()和Object.defineProperties()可以更为精确的指定对象属性(property)的属性(attribute)(可怜的汉语-_-!)。特别是,getter和setter属性,用语法级的东西是做不到的。
数组是动态长度的。
function中的this是动态绑定的是被调用对象,o.f()中f里的this是o,但var tf = o.f; tf()中this就不是o了,是全局对象。而且f中如果调用了其他函数ff,那么ff中的this也不是o,如果ff中需要使用对象o,则需要在f中自行保存this到某个变量中。这个和在C#中用delegate的效果不同。如果想获得类似C#中delegate进行调用的效果,可使用var g = o.f.bind(o); g()中的this就是o了。
由于语言的弱类型性,判断某个对象是否可以进行某种操作更常见的用法为判断对象上是否有某个属性或方法,如果有就直接进行调用。不像强类型语言,更常用的方法为检查某个对象是否为某个类的实例。
不同窗口中的语言预定义的对象不是同一个对象,判断某对象是否为某类的实例时需要注意。
语言本身是没有多线程的,一个函数调用栈没有彻底返回之前,其他函数是不可能执行的。所以各种可能耗费时间的动作(如网络访问等),浏览器提供了异步接口,由浏览器来处理多任务的并发执行。所以如setInterval,setTimeout等函数如果传入了一个死循环,那么一旦触发,其他任何函数都再也不会执行了;再如某个事件可以先启动再绑定回调函数,但这不是好的习惯。
可以使用Web Worker来进行无共享数据的有限的多线程动作,new Worker("TheWorkerJSFile.js");可获得Worker对象。数据通讯必须通过这个Worker对象的postMessage方法来发出消息,onmessage事件来获取消息,postMessage方法的参数会被复制以保证无数据相关(Chrome提供了对象所有权转移的postMessage,会把参数对象的控制权直接转给Worker对象,从而避免了数据复制)。
类型数组Typed Array更类似于强类型语言中的数组,如Uint32Array,Float64Array等。这些类型数组是ArrayBuffer的一个数据解释包装器,ArrayBuffer是二进制数据实际存储的地方,大小必须在初始化的地方制定,并且无法动态修改,所以类型数组也是如此。一个ArrayBuffer可以用多个不同类型的Typed Array来包装,从而对二进制进行不同的解释。DataView是另一种ArrayBuffer的包装器,可以按照指定类型读写指定位置的数据。
Blob是一个二进制块的抽象,可以指定其MINE type。File是其子类。Blob可以通过XMLHttpRequest来传送,从而可以方便的传送二进制数据块。使用FileReader对Blob进行读取,使用BlobBuilder来创建Blob,这两者都可以处理Blob与ArrayBuffer的转换,不过FileReader是异步的,需要定义事件处理器。还可以从Blob中获取blob URL,以供如图片等使用。
scope chain
首先来看个小例子,Fibonacci数列迭代器:(makeFibonacciIterator的局部变量a和b被返回的匿名函数所引用,每次调用所返回的匿名函数都会修改已经返回了的函数makeFibonacciIterator的局部变量a和b。)
function makeFibonacciIterator() { var a = 0; var b = 1; return { next: function () { var t = b; b = a + b; a = t; return t; } }; } var it0 = makeFibonacciIterator(); var it1 = makeFibonacciIterator(); console.log(it0.next()); // 1 console.log(it0.next()); // 1 console.log(it0.next()); // 2 console.log(it0.next()); // 3 for (var i = 0; i < 10; ++i) { console.log(it1.next()); // 1 1 2 3 5 8 13 21 34 55 } console.log(it0.next()); // 5 console.log(it0.next()); // 8 console.log(it1.next()); // 89
如上节中提到的,函数定义时可以捕获定义的地方的所有变量(级联到全局对象为止)。
如果函数返回了一个直接或间接包含包含了本地变量的函数的东西,那么函数内部被引用的变量在返回之后仍然存在(也就不会被gc)。
每次调用函数都重新建立那个函数的scope,如上例中的it0和it1,这两个迭代器产生的数列互不影响。
再看一个典型的错误例子:
// Want to get a list of functions that returns 2 * function index, but the result is wrong. function makeFunctionList() { var list = []; for (var i = 0; i < 3; ++i) { list[i] = function () { // Notice here, i reference to local variable // and won't keep that value when appending to list. return 2 * i; }; } return list; } var l = makeFunctionList(); for (var i in l) { console.log(l[i]()); // We get three 6 here. }
list中的每个函数都直接引用了函数makeFucntionList的同一个局部变量i,这样当第一个函数保存到list中的时候i为0,但第二个函数保存的时候i就变了。所以返回的list中的函数调用都返回相同的值。
正确的方法:
function makeFunctionList() { var list = []; for (var i = 0; i < 3; ++i) { // Here we use a function to create a scope to keep loop variable value. list[i] = (function (j) { return function () { return 2 * j; }; }) (i); } return list; } var l = makeFunctionList(); for (var i in l) { console.log(l[i]()); // 0 2 4 }
这样变量j就会引用中间的那个函数的参数j,而这个函数每次循环都会创建一个新的。
prototype chain
几乎任意一个对象都有一个proto原型对象(这个原型对象在chrome或者firefox中可以用对象的__proto__属性获取),这个原型对象就是创建这个对象的构造函数的prototype属性所指的对象,而不是构造函数本身。(用Object.create(null)这种方法创建出来的对象没有原型,所以用了“几乎”这个词)
原型对象上都会有个constructor属性指向构造函数,而构造函数上都会有个prototype属性指向这个原型对象。注意构造函数上也有个constructor属性,这个constructor是构造函数的原型对象上的constructor属性,指向Function这个构造函数。
访问一个对象上的属性时,首先会查找对象上有没有这个属性,没有的话查找其原型对象上,再没有的话查找这个原型对象上的原型对象上有没有,以此类推。这就是原型链,prototype chain。这也就意味着原型对象上的属性是可以被覆盖(override,不过与传统的编译语言的虚函数的实现方式并不相同)的。
对一个对象的某个属性赋值时,如果这个属性是其原型对象上的属性,那么则会在这个对象本身上添加这个属性。
原型对象上的属性会被所有原型是这个原型对象的对象所共享。从而通用的属性,如函数等,放在原型对象上可以更加节省空间。
用构造函数创建对象时要用new来进行调用,这样会先创建一个空对象,把这个空对象的原型指向构造函数的prototype对象,然后这个对象被绑定为构造函数的this,然后对构造函数进行调用,构造函数的返回值被忽略,new语句返回这个新创建的对象。
进行类(这里指构造函数)继承时,一种good practice为:定义构造函数,把构造函数的prototype指向所要继承的类(构造函数)的一个实例(通常是新创建的),然后把这个prototype的constructor指向构造函数自己,然后再在这个prototype对象上追加这个类所需要的新方法,与强类型语言中的类的静态函数和变量对应的函数与变量可以放到构造函数上。
最后写个小例子,把上一节中的那个例子改成模仿传统面向对象的实现方式:
// Abstract base class is only for illustration purpose. function AbstractIterator() { } AbstractIterator.prototype.next = function() { return false; }; Object.defineProperty(AbstractIterator.prototype, "value", { get: function() { throw new Error("Method not implemented."); } }); function FibonacciIterator() { this.a = 0; this.b = 1; } FibonacciIterator.prototype = new AbstractIterator(); FibonacciIterator.prototype.constructor = FibonacciIterator; FibonacciIterator.prototype.next = function () { var t = this.a + this.b; this.a = this.b; this.b = t; return true; }; Object.defineProperty(FibonacciIterator.prototype, "value", { get: function() { return this.a; } }); var fi0 = new FibonacciIterator(); var fi1 = new FibonacciIterator(); // fi0.next is always true. for (var i = 0; fi0.next() && i < 10; ++i) { console.log(fi0.value); } // Warning, variable i and the one in the loop above is the same one. Be careful. for (var i = 0; fi1.next() && i < 10; ++i) { console.log(fi1.value); }
这个例子可以看做上面说到的good practice的一个示例吧。
结束的分割线
刚学时间不长,也没有实战经验,所以不可避免的会有各种问题,各种错误、不合理的地方、更好的方式与理念等,欢迎提出!