javascript特性杂谈
发布日期:2021-06-29 02:35:16 浏览次数:2 分类:技术文章

本文共 11858 字,大约阅读时间需要 39 分钟。

目录

最近语言学习有些疯狂, 从Ruby到Lisp, 然后是C#, 既然已经疯狂了, 就顺面学习一下吧. 对javascript的印象一直不佳, 从骂脏话最多的使用者, 到使用者平反的世界上, 从所谓的让人抓狂的特性, 到世界上任何可以用javascript实现的东西, 最终都会被javascript实现, 并且, 这是最后一个实现. 出处太多, 不一一列举, 知者已知, 不知者也没有必要为了这些无聊的言论特意找出处了.

其实也不是完全没有用过javascript, 以前在开发一个Unity项目的时候用过一下Unity里面的javascript, 只不过那个javascript我甚至都只能称之为UnityScript. 太多太多自己实现的特性, 而又有些不够完整. 现在, 认识一下真正的javascript吧.

环境

Mac OS X 10.8.2, node v0.8.16

需要解释一下, node跟浏览器里嵌入的javascript不一样, 不具有类似confirmprompt等接口, 我用console.log来输出.

概要

JavaScript本身就是设计为一个前端语言, 据说设计只用了10天, 有些缺陷, 但是的确足够简单. 虽然和大多数的语言书籍一样厚如砖头, 但是其实语言本身的介绍只有前面近200页, 这个厚度其实也就和中描述的C语言差不多.

也就是因为设计比较简单, JavaScript也被一些人认为不算是现代语言, 不具有现代语言的一些特性.

语法细节

  1. 可选的语句结束符;, 这个很少见. 不过一般的规范都推荐不要真的省.
  2. 支持自增++,自减符号--, 相对Ruby, Python来说, 这个要更习惯.
  3. switch和传统的C语言语法类似, 但是可以支持字符串的case.
  4. 支持NaN, null, undefined这三种表示类似无意义的量的方式, 有的时候这是混乱的根源. 也许还要再加上Infinity.
  5. 与大部分语言一样, javascript也分为原生类型和引用类型, 其中原生类型在拷贝, 参数传递和比较时时通过值的方式, 而引用类型都是通过引用的方式.
  6. 字符串为不可变类型, 任何的改变处理都是生成新字符串. 比较时为值比较. 字符串的值比较我个人认为时更加自然的做法, 比Java那种变态的方式要自然的多. 你几乎要反复的告诉每一个新来的程序员, 字符串的值比较在java中要使用equals函数.
  7. 动态类型语言, 变量通过var定义.
  8. 支持用label方式的break和continue, 用于在多层循环中直接对外层循环进行break和continue.
  9. 完整并且传统的try, catch, finally异常机制. 除了C++没有finally不够完整以外, 几乎所有现在语言的异常都是这么设计的了.

字符串

javascript虽然说语法是类C的, 但是起点是, 所以尽管设计的面向对象系统虽然不是传统的模版式的, 但是javascript中的字符串都是对象.

"hello, world".length// out: 12

上述的代码在现在已经不稀奇了, 但是相对C++来说还是更先进的.(可见C++多落后了)

var str = "hello" + "," + "world!";console.log(str);// out: hello,world!

字符串支持+操作符作为字符串连接.

javascript有个奇怪的地方是字符串和数字同时使用时:

console.log("3" + 4 + 5);// out: 345console.log(4 + 5 + "3");// out: 93

也就是说, 相对一些语言(比如php)会自动的将字符串转为数字来说, javascript是倾向于将数字转为字符串的. 其实因为这种用法过于灵活, 即使是Ruby和这样以灵活著称的语言都是不允许这样的自动类型转换的.

更诡异的还不只这些, 对于加法来说是如此, 对于乘法来说又是另外一回事:

console.log("3" * 4);// out: 12console.log("3" * "4");// out: 12

在乘法运算中, 因为javascript的字符串并没有像Ruby, Python一样对乘法的运算做出特殊解释(字符串的乘法表示重复), 所以默认会将字符串转为整数进行运算, 更诡异的是, 就算是两个字符串, 同样也会不报错的进行整数转换并且运算.

函数

函数在javascript中是第一类值, 同时还支持闭包. 这是javascript构成对象的基础.

function add(x, y) {
return x + y;}var sub = function(x, y) {
return x - y;}add(2, 3);sub(5, 3);// out: 5// out: 2

有上述两种函数构造形式, 在调用时没有区别. 其中第一种方法和传统的函数定义方式一样, 而第二种实际上就是匿名函数的定义方式了. 只不过因为javascript中函数是第一类值, 所以可以很方便的赋值.

匿名函数

匿名函数也被称为lambda, 是个很方便和有用的特性, 加上对闭包的支持, 以此衍生了很多特性. 也因此成就了javascript类函数语言的特性.

var caller = function(fun, leftParam, rightParam) {
fun(leftParam, rightParam);}caller(function(a, b) {
console.log(a+b); }, 10, 20);// out: 30

如上例所示, 匿名函数很重要的一个应用就是用于很方便的构建高阶函数. 也许上例有些太生造, 最常用的一个特性可能就是排序了, 因为排序的规则可能很多, 一般排序函数都允许再传入一个函数作为参数, 来指定排序的规则. 比如再javascript中, 普通的排序函数有些奇怪, 默认是按照字符串排序的. 见下例:

a = [1, 3, 2, 10, 20];console.log(a.sort());// out: [ 1, 10, 2, 20, 3 ]

这在大部分时候估计都不是我们要的做法, 默认这样子我是第一次看见, 这就像字符串和整数想加最后变成字符串一样诡异, 也许javascript本身设计的时候是作为前端检验表单啥为主的语言, 所以对字符串这么偏爱吧. 幸运的是, sort函数还是可以传入一个函数作为排序规则的. 见下例:

a = [1, 3, 2, 10, 20];console.log( a.sort( function(a, b) {
return a - b; } ) );// out: [ 1, 2, 3, 10, 20 ]

因为匿名函数和递归在javascript中使用的都比一般语言要多, 所以提供了arguments.callee用于表示当前调用的函数, 以方便匿名函数的递归调用, 事实上, 相对一般用函数名的递归调用方式, 这种方式要更加符合DRY(Dont Repeat Yourself)原则, 因为当函数名更改的时候, 不用再更改递归调用的函数名了.

var factorial = function(n) {
if (n <= 1) {
return 1; } else {
return n * arguments.callee(n - 1); }}factorial(4);// out: 24

更有意思的是, arguments.callee在javascript的中是禁止的, 简单的说就是这种调用方法是官方不推荐使用的错误用法, 在将来甚至有可能废除, mozilla的解释是这种更DRY的用例本身很”weak”, 但是却阻止了inline优化的进行, 因为这种方式是通过引用un-inlined函数实现的, 也只有函数un-inlined时, arguments.callee才可以引用到.

事实上, 我觉得这简直是因噎废食的做法, 因为现在虽然是这样实现的, 但是完全可以通过更好的语法分析, 然后进行编译器的优化, 而不是因此废弃这样有用的语法. 这种用法绝对不像是官方说的那么”weak”, 要知道, DRY几乎是软件设计领域头等重要的原则.

闭包

一个闭包就是一个函数和被创建的函数中的范围对象的组合. 因为闭包的强大特性和带来的方便, 很多传统的语言都逐渐了加入了对其的支持, 很多时候, 甚至被视为一个语言是否还算是跟上时代的标志.

function makeIncrementor(base) {
var count = base; return function(num) {
count += num; return count; }}obj1 = makeIncrementor(10);obj2 = makeIncrementor(20);obj1(1);// out: 11obj1(1);// out: 12obj2(2);// out: 22obj2(2);// out: 24

上面的例子较好的展示了闭包的特性, 可以获得上层函数的参数和变量, 并且各自互相独立, 因为闭包对局部状态的保存, 很多时候能当作一个对象来使用.

灵活的参数调用

function add(x, y) {
return x + y;}add(2, 3, 4);add();add(2);// out: 5// out: NaN// out: NaN

上述代码在调用时不会发生错误, 而是直接把后面的参数抛弃掉.

甚至于, 后面的两个参数不够的函数调用, 会返回NaN, 也不会发生错误.
本质上是因为一旦函数调用参数不够时, 后面的参数都会被置为undefined. 所以虽然javascript不支持默认参数, 但是可以模拟出来.

function mul(x, y) {
if (y === undefined) {
return x * 10; } return x * y;}mul(10);// out: 100

更灵活的语法是可以通过arguments变量来获取参数, 这样可以支持任意数量的函数参数.

function add() {
var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i]; } return sum;}add(2, 3, 4, 5);// out: 14

函数级作用域

javascript只有函数级别的作用域, 函数外都是全局作用域, 没有块级作用域. 意味着类似for, while, if等块中定义的实际是全局变量. 这个设定在现代语言中是逆天的. 于是, 借助匿名函数, 人们想出了更加诡异的解决方案来模拟块级作用域.

for (var i = 0; i < 10; ++i) {
console.log(i);}console.log(i); // 此时i仍然可用.(function() {
for (var j = 0; j < 10; ++j) {
console.log(j); } })();console.log(j); // ReferenceError: j is not defined.

数组

javascript的数组比想象的要灵活, 支持用超出索引的引用来添加元素, 这个我只在ruby和php中见过, 连python都不支持. 当然, 这种设计虽然灵活, 但是容易出现很隐晦的错误, 最终是好是坏也难以评价.

a = [0, 1, 2];a[a.length] = 3;a.push(4);console.log(a);// [ 0, 1, 2, 3, 4 ]

上述两种在数组后面添加元素的方法是等效的, 假如添加的元素不是数组的下一个元素(即跳跃式添加的话), 中间会用undefined填充.

对象

javascript的对象本质上就是一个hash表的集合.

var obj = new Object();var obj2 = {};

有上述两种语法用于创建空对象, 其中第二种被称为’literal’语法, 也是常用的数据格式Json的基础.

因为是hash表, 所以动态添加内容不在话下.

var obj = new Object();obj.name = "Simon";obj.hello = function() {
console.log("hello," + this.name);}obj.hello();// out: hello,Simon.

有些不一样的是, 因为javascript中函数是第一类值, 所以可以很自然的在这个对象中添加函数, 完成完整的数据封装. 用{}来初始化上述对象的话, 会更加简单:

var obj = {
name : "Simon", hello : function() {
console.log("hello," + this.name); }}

正因为其实对象就是一个关联数组, 所以同样可以用for in来遍历, 作用就像是python中的dir一样. 类似这种自审视的功能, 在传统静态语言是较为稀缺的, 在那种语言里这种功能叫做反射. 相配套的还有typeof操作符, hasOwnPropertypropertyIsEnumerableisPrototypeof函数.

for (var name in obj) {
console.log(name);}// out: name// out: hello

更进一步, 你甚至可以通过obj["hello"]()这种调用关联数组的方式来调用对象中的函数.

面向对象

javascript算是第一个让大家知道这个世界上除了从C++一派来的class-based(模版式)的类定义方式, 还有类似self语言的prototype(原型)方式的流行语言. 虽然lua也是prototype方式, 但是毕竟只在游戏圈子里面流行.

自定义对象:

function Rectangle(w, h) {
this.width = w; this.height = h; this.area = function() {
return this.width * this.height; }}var rect1 = new Rectangle(2, 4);var rect2 = new Rectangle(8.5, 11);console.log(rect1.are());// out: 8

以上代码用类似构造函数的方式处创建了两个类型为Rectangle的对象. 注意和以前创建对象的区别, 以前我们都是从Object直接开始构建, 那样在构建多个对象时远不如这种构造函数方式方便. 用这种方法, 我们就能得到简单的类似class-based对象创建的方法. 只是创建的是构造函数, 不是一个class而已.

不过, 上面代码并不完美, 最大的问题在于每个创建的对象都有一个自己的area函数, 而实际上, 所有的对象只需要指向一个共同的area函数即可, 这也是C++等class-based语言的做法. 为每个对象都创建一个函数, 无论是运行效率还是内存占用效率都不恰当. javascript提供的解决方案就是prototype, 在函数对象中默认都会初始化一个prototype的变量, 这个变量中有的所有函数最终都会被这个函数新创建的对象拥有, 并且拥有的还都是引用, 见代码:

function Rectangle(w, h) {
this.width = w; this.height = h;}Rectangle.prototype.area = function() {
return this.width * this.height; };var rect1 = new Rectangle(2, 4);console.log(rect1.are());// out: 8

类属性(Class Properties)

在class-based语言中, 有的属性可以直接通过类名使用, 并且一个类的所有对象共享同一个对象. 在javascript因为所有的函数本身就是对象, 构造函数也不例外, 所以可以通过在构造函数上直接添加属性来实现这样的特性.

Rectangle.UNIT = new Rectangle(1, 1);

其实, 类似的用法javascript本身就有, 比如Number.MAX_VALUE就是这样的类属性.

类方法(Class Methods)

和类属性一样, 在构造函数上创建对象, 就能模拟出class-based语言中类方法. 这里不累述了.

私有成员(Private Members)

在class-based语言中(Python例外), 一般都有对不同的成员设置不同访问权限的方法. 比如C++的prvate, public, protected, 在javascript, 通过上述方式创建的对象, 你可以看成都是默认为public的, 但是也的确有办法让外部访问不了内部的变量.

简单的方法

此方法来自, 代码如下:

function Rectangle(w, h) {
this.getWidth = function() {
return w; } this.getHeight = function() {
return h; }}Rectangle.prototype.area = function() {
return this.getWidth() * this.getHeight();}var rect = new Rectangle(2, 3);console.log( rect.area() );// out: 6

此时, 无论是在对象外还是在对象内部, 都只能通过访问函数(getWidthgetHeight)获得成员变量.

Crockford的办法

事实上, 上面的简单办法没有在根本上解决问题, 只是限定需要通过访问函数了而已, 外部还是能访问对象的内部变量. Crckford据说是第一个发现了javascript创建真正私有变量的技术. 该技术主要在有描述.

function Rectangle(w, h) {
var width = w; var height = h; this.area = function() {
return width * height; }}var rect = new Rectangle(2, 3);console.log(rect.area());

该方法利用了javascript的闭包特性, 此时widthheight在外部彻底无法访问. 只有函数内部才能访问. 同样的, 私有的函数也可以通过一样的方法实现, 但是, 这个方法我感觉还是不够完美, 因为很显然的原因, 此时需要访问私有变量的函数都只能在构造函数中直接定义, 不能再使用prototype变量了, 也就是会有前面提到的每个对象都会有个新函数的问题.

模拟class-based的继承

相关内容见Douglas Crockford的. 我个人因为对这种奇怪的方式比较反感, 所以不太想去使用, 这里就不进行描述了. 需要提及的是, 假如真的需要class-based的继承的话, 在最新版的javascript 2.0规范(ECMAScript 5)中你能找到你想要的真正的类. 虽然相关标准还在进行当中, 可能还需要几年才能实际使用.

语言的发展道路基本上是趋同的, 程序社区有较为公认的标准, 所以PHP也在新的版本中加入了完整的面向对象支持, 而C++在11标准里面加入了闭包. 而Java和C#在新版本中不仅加入了闭包, 还增加了模版.
当javascript 2.0加入了class以后, 可能将来使用javascript就和C++等语言区别不大了. 可能会更像.

基于原型的继承

这种继承方式和class-based的继承不一样, 直接使用了javascript的prototype特性, 在真正的class没有出来之前, 我个人对这样的方式好感更多. 主要可以参考的还是Douglas Crockford的文章, 见

简单的说就是子类讲自己的prototype变量设为想要继承的父类对象, 根据javascript的特性, 当一个函数找不到时, 会在prototype中寻找, 也就相当于子类没有重载时直接使用父类的函数. 当一个函数可以找到时, 会直接使用子类的函数, 这相当于子类重载了父类的相关函数.
因为我还是不准备使用这个方法, 所以这里还是不加描述了.

极简主义法

该方法我第一次是在上看到的, 见.

就阮一峰描述, 该方法最先由荷兰人Gabor de Mooij在中提出.
该方法不使用Object.create和prototype特性, 本质上是新增一个自己约定的构造函数, 自己模拟了一个类似prototype的原型机制, 代码相对来说比较简单容易理解. 但是其实已经颠覆了本节前面提到的所有内容, 比如没有使用prototype和new.

类的创建

上面的Rectange类, 可以改为下面的方式实现.

var Rectangle = {
createNew: function(w, h) {
var rect = {}; rect.width = w; rect.height = h; rect.area = function() {
return this.width * this.height; }; return rect; } } var rect = Rectangle.createNew(2, 3); console.log(rect.area());

继承

需要注意的是, 此时的Rectangle是一个单纯的对象, 而不再如传统方式一样是一个函数(当然, 其实也是对象). 这也是一个更容易理解的优点. 然后, 我们可以简单的在子类的createNew函数中先创建出要继承的对象, 然后继续修改该对象直到达到我们的要求. 如下:

var SubRectangle = {
createNew: function(w, h) {
var rect = Rectangle.createNew(w, h); rect.perimeter = function() {
return this.width * 2 + this.height * 2; }; return rect; }}var rect = SubRectangle.createNew(2, 3);console.log(rect.area());console.log(rect.perimeter());

私有成员及类的属性

我在新的SubRectangle子类中新增了perimeter函数, 用于计算周长, 可以看到使用的方法和传统的继承非常的像.

根据这个思路和前面提到的传统方法, 私有变量和类的属性, 方法都是很简单的事情.

var Rectangle = {
createNew: function(w, h) {
var rect = {}; var width = w; // private var height = h; // private rect.area = function() {
return width * height; }; return rect; } } Rectangle.UNIT = Rectangle.createNew(1, 1); // class propertie var rect = Rectangle.createNew(2, 3); console.log(rect.area()); console.log(Rectangle.UNIT.area());

共用函数

这个方法总的来说非常简单直观和直接, 没有必要像Douglas Crockford的方法一样需要创建较多的辅助函数来实现. 但是, 其实还是像前面没有用到prototype的解决方法一样, 每个对象的成员函数都是独立的, 假如对效率和内存比较在意的话, 可以使用引用外部函数的方法来优化. 如下:

var Rectangle = {
_area: function() {
return this.width * this.height; }, createNew: function(w, h) {
var rect = {}; rect.width = w; rect.height = h; rect.area = Rectangle._area; return rect; } }

这个方法能解决函数有多份的问题, 但是同时带来的问题就是无法访问私有成员, 同时会给外部的Rectangle增加一些接口, 虽然可以通过命名来告诉调用者, 这些接口是私有的. 具体用哪种方法, 就看是注重效率还是注重代码本身的设计了.

小结

javascript本身设计的时候是一门简单的语言, 所以没有加入类似C++的对象系统, 而是选择了self的原型方式, 这样能极大的简化语言本身的设计. 但是, 随着javascript的应用原来越广泛, javascript本身的简单性及对模块性编写缺乏支持的问题愈显严重, 于是智慧的程序员们想出了很多办法, 上面关于面向对象一节就是各地的大牛们想出的办法, 其实这样各式各样的解决方案本身就反应了javascript语言特性的缺乏问题. 直到上面最后一种方案, 算是基本能用, 可是还是不够完美.

很多javascript的使用者大力的鼓吹说javascript其实也能适应大规模编程的需求, 是一门优秀的语言, 我并不认同. 当钻入语法的细节之中, 尝试模拟语言本身不支持的东西成为常态以后, 这其实就说明了语言本身有问题. 就像boost库不停的在探索C++的极限一样, 那就是因为C++进化的太慢了. 起码, 我在学习C#的时候, 光是学习语法就好了, 不需要用C#去模拟什么.
从社区上看, 一个良性的语言社区应该像Ruby和Python社区一样, 大部分人最关注的东西都是用语言去实现的东西(业务本身), 一小部分人关注的是库的编写, 几乎没有人去关系语法本身的东西. 而javascript和C++社区在这点上都有问题, 这也是因为语言本身的问题.

参考

 5th Edition,(我看的是, 上面链接是最新的第6版. David Flanagan著, OREILLY

,(上面链接的是最新的第3版) Nicholas C.Zakas著, 人民邮电出版社

writen by 

转载地址:https://blog.csdn.net/yyyyyyyyyywwwwwwwwww/article/details/51200617 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:ppp拨号脚本配置
下一篇:linux ppp上网的文章

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月02日 01时53分30秒