JS语言精粹读后体会
一
先说说JS是容易被误解的语言的观点。从名字上来看,”Java”这似乎暗示着这门语言与Java的关系;好像这门语言是Java的子集,或比Java弱。但实际与Java并无关系,如果说非要有关系,那可以说说一点八卦。当年Netscape确实为了让它快速发展,与Sun公司有过合作;其中为了借大热的Java东风,改名Javascript(原来叫做Livescript).JS第一版仅仅用了10天,未经打磨就广泛使用至今(什么是”网红”,这才是”网红”)。JS不可避免的有许多设计失误,比如全局变量的设计模式。标准的出现是晚于JS的,而且标准也并不”标准”,这导致了许多滥用。由于JS通常是用在web的,从而被认为只能用于浏览器,但它却可以用在服务器等地方的。它的语法类C,但实际上是”披着C外衣的Lisp”,是一种面向对象的函数式编程语言。与传统的面向对象的语言相比,它不是基于类的继承,而是基于原型的继承。
公司是不会让人使用语言的每个特性的,我们要使用那部分最可靠、可维护、方便阅读的成熟的子集。
为什么要使用JS?一方面是没有选择;另一方面是这门语言本身有 它自身的优雅、灵活的地方,具有极强的表达能力。
DC认为JS有许多优秀的想法:函数、弱类型、动态对象、字面量的对象表示法。糟糕的想法是:基于全局变量的编程模式。JS是披着C外衣的Lisp(基于lambda演算的语言,与人工智能极为相关)。
强类型语言可以在编译阶段找到程序的类型错误,弱类型做不到;但通常类型错误不是最主要的问题。弱类型可以让人不必疲于应对造型系统。
对象的字面量表示法很好用,DC也借鉴到了JSON中作为一种代替XML通信的数据传输格式。JS的连接单元是基于全局变量的。编译单元的顶级变量会到全局对象这个公共命名空间。
二
这章主要讲语法,DC使用了表示形式语法的巴克斯范式图,这的确比语言来得准确简洁。我只想表达一个问题:表达式与表达式语句的关系。因为其他问题很明白。
从DC的巴科斯范式图上大致可以认为,表达式是比表达式语句宽泛的。DC的子集中表达式语句是表达式的真子集。
表达式语句主要用于单个或多个标识符赋值、函数或方法调用、删除对象的属性,语法开头是标识符。
表达式:字面量、标识符、加上圆括号的表达式、prefix+expression、expression+infix+expression、expression+”?”+expression+”:”+expression、expression调用、expressio属性提取、new expression 调用、delete + expression+refinement.
具体的巴科斯范式图我就不画了,详见JS语言精粹。
三
基本问题
JS的数据类型(不是数据结构)分:简单数据类型(undefined\null\boolean\string\number\symbol)、复杂数据类型(object)
对象是可变的键值对的集合,最关键是对象是无类型(class-free)的,也就是说对于新的属性值类型没有要求。属性名可以是name(标识符)、string(包括空字符串),很适合各类数据管理。最牛逼的机制是对象之间的非传统意义上的继承或是说委托更好一些;可以减少对象初始化的时间和内存。而且这是一种不同于类的新的编程思路。
对象的字面量表示法的属性名类型是字符串,虽然有时候输入的可能并不是字符串,但实际上是被转为了字符串;但若不符合标识符规则时就要显式的使用字符串。这在检索时会表现出来。对象查询属性时若找不到(包括原型链)返回undefined。可以使用”[]”、”.”来获得,符合name规范时使用点访问法;其余使用[]访问法。若已有属性会被覆盖,若不含属性可以新建属性。
对象在堆内存中不会被复制,复制的是对象的引用。所以传递的函数、数组等都是引用而已。
原型链问题
原型链是一种内部机制,我在我得第一篇博客已经表达过这个问题。就算根本不设置也会存在。对象有设置对象原型的方法DC原来提出有:
if ( typeof Object.beget !== "undefined") {
Object.create = function (obj) {
function F () {}
F.prototype = obj;
return new F();
}
}
另外,必须指出一下问题:使用返回的new F()的内部原型链由F.prototype更新。原型关系是一种动态关系,原型链中更新的时候,查找时会对其他立即显示。对象属性的查找先从对象开始,然后会向原型链中查找。可以使用typeof来大致做一下过滤。
可以使用for/in语句来枚举对象的属性,但不能保证出来的顺序;同时还会枚举原型链的。可使typeOf和obj.hasOwnProperty()来过滤。如:
for ( var key in obj ) {
if (obj.hasOwnProperty(key)) {
if ( typeof key !=="function") {
//代码
}
}
}
可以使用delete来删除对象的属性,而不会修改原型链
减少全局变量污染
由于JS没有连接器,编译单元的顶级变量会出在全局变量中。为了减少变量污染,可以一个公共的接口来包含全局资源或使用闭包(与词法作用域相关)。
四
函数
函数是对象,它与其它对象唯一的不同是它可以调用。函数可实现:代码复用、信息隐藏、代码组合调用。建立函数时会建立:上下文、调用函数的代码。每个函数(除Function.prototype)都会有一个原型对象。
function foo ( ) {
//code
}
foo.prototype = {constructor:this};
它必有一个foo.prototype对象,而且这是显含的。函数字面量属于字面量,也就属于表达式的范畴。从而可以使用表达式的地方就可以使用函数字面量。
JS函数的参数是按值传递的。它不会检查传入实参的类型、个数;若实参少于形参实参函数调用时的形参值为undefined.每个函数会this,this在函数执行时才会确定,与函数的调用方式相关(与动态作用域有某种相似)。调用方式分为:方法调用、函数调用、构造器调用、apply调用。
方法调用:也就是说一个函数为某个对象的属性值。那么函数调用时的this到底是哪个? 我要再次强调,对象会有自己默认的原型链,同时我们可以设置对象的prototype属性,但这个属性与内部的原型链相比很弱,对象的”继承”或更确切的说是委托是基于内部原型链的。
var a = {};
var b = {"name":"name"};
a.prototype = b;
console.log(a.name) //undefined
Object.setPrototypeOf(a,b);
console.log(a.name) //"name"
开始,我直接设置prototype属性,然后以为通过对象的继承可以访问到对象b中的name;事实是:此时a与b的原型链链接到了Object.prototype。接着,我真正修改了原型链,故可以访问到name属性。
那么,这个表面的prototype有何用? 前文提到函数(除Function.prototype)总会有一个对象与之对应。若我建立一个用函数建立一个对象的话,就会有用。新建的对象的原型链中的上一环为构造函数的prototype对象。
function foo () {}
//那么一定有一个foo.prototype对象。
var b = new foo();
该对象的原型链中就会有 b 的存在
foo.prototype.isPrototypeOf(b) //true
其三,一般函数的原型链中不含prototype对象。但Function.prototype为function(){},且位于Function的原型链中。
Function.prototype.isPrototypeOf(Function) //true.
回到刚才的问题,方法调用函数时,对应的this为何值。我直接给出结论,我认为是:可以直接查找到方法的那个对象。直接上代码。
var a = {"a":"a"};
var b = {"b":"b"};
var c = {"c":"c",
"show":function(){
return this;
}
};
Object.setPrototypeOf(b,c);
Object.setPrototypeOf(a,b);
//现在原型链从a开始到b到c到Object.prototype.
a.show(); //对象a
b.show(); //对象b
//Function的例子
Function.prototype.method = function (name,func) {
this.prototype[name] = func;
return this;
};
// this与调用方式有关
再有:
Function.prototype // function () {}
Function.protoype.demo = function () {
console.log(this);
};
//可以通过Function这个函数访问到demo方法,
Function.demo() // function Function() { [native code] }
Function.prototype.demo() // function () {}
所以,不是方法在那个对象中this就是哪个。
函数调用的时候的this值 直接调用函数是this是全局对象而非该函数的外围的this值,ES6中有语法糖来处理这个问题。
构造函数模式的this new + 构造函数会形成一个对象,这里忽略其他细节;只讲this值,这时的this是新建立的那个对象
function foo (name) {
this.name = name;
}
var obj = new foo ();
//也就是说foo执行时的this是obj,从而才有给obj添加name属性的可能
构造函数与一般函数没有区别,但是若一个函数建立的目的是用于建立对象,应当把其首字母大写;否则直接调用可能会给全局对象添加属性。
apply调用模式 函数是对象,所以可以有方法,apply/call就是方法。伪代码:function.apply(this,argumnets),function.call(this,p1,p1,p1)。
记忆
记忆这种优化手段真是让人兴奋,它可以大大减少计算的工作量,例如在阶乘、裴波那契数列的计算中,若不采用优化,那浏览器甚至算不出来。采用优化后秒算出结果。下面是通用的记忆器
function memoizer (memo,formula) {
var recur = function (n) {
var result = memo[n];
if ( typeof result !== "number") {
result = formula();
memo[n] = result;
}
return result;
}
return recur;
}
JS的函数化很强大,这种方法在一个个函数的基础上差异化继承,也可以增强;也是建立部件的基础。实现对象与它的功能模块的扩充。建立对象与方法之间的松散耦合。
var o = {name:"demo"};
var enhance = function (o) {
var that = o;
if ( !o.name) {
o.name = "initial";
}
if ( !o.get_name) {
o.get_name = function () {
return o.name;
};
}
return that;
};
var b = enhance(o);
五
方法
数组方法
模拟队列的操作:push()/shift();unshift()/pop();模拟栈操作:push()/pop(); push()返回增加后的长度、unshift也是。pop和shift返回项值。concat()在已有的数组基础上返回一个数组,即是传入的是数组,它也会将数组变成单一的参数。reverse()没有用,join()将数组以某种分隔符的形式返回字符串。字符串的split可返回一个数组。
slice在字符串及数组上都会有,slice(n,n+p),p是复制数目。数组会得到一个数组,即是是空数组,这个方法可以处理负数。字符的subString()可以复制,但不支持数组,不建议用。splice()返回数组,会影响原来的数组,可以修改、添加、插入项与数组中,但效率不高。sort()默认基于编码大小,可以认为传入函数方便比较。
Number的方法
toString()。该方法将数字以基于几进制的形式输出。实现起来有bug.若是整数会出错(下面几个方法也是这样),但若是浮点数形式没问题。存在变量中也不会有问题,故最好现存于变量中,如:23.toFixed(3) 会出错,而23..toFixed(3)就可使用。 toExponential()以科学计数法输出字符串,可指定小数点后的位数(0-20位)。有数字直接使用方法的可能。
toFixed()将数字指定小数点后的位数输出。 toPrecision()指定总的位数。
正则表达式方法
test()/exec();test()不建议使用g-flag。exec()好用,但慢(捕获较慢)。exec()返回一个数组,index0为匹配项,后面一次是捕获组、index、input。不用全局标识仅仅匹配第一次的。g标识下会一直向下,但只是每次匹配一项。
字符串的操作
字符串是最常见的操作。charAt()/charCodeAt()/String.fromCharCode()得字符串。concat()拼接。建议使用“+”。 查询:indexOf(text,position)/lastIndexOf()指定字符串及开始位置,返回位置;查询不到返回-1。search(exp)使用正则表达式,忽略g-flag,返回位置。
match()会返回一个数组,正则表达式中没有g同没有g的exec()方法;含g会有返回项各匹配项。replace(a,b)中a可以是字符串、正则表达式。b可以是函数、可以是字符串。同样,若不带g只会匹配第一项。为字符串时$有特殊含义。使用函数时,传入第一个参数是匹配项,然后是捕获组。
split()可以是字符串、正则表达式。是正则表达式时捕获组会加入数组。