后端程序员的 JavaScript 之旅 - 好的设计与坏的设计

好的设计与坏的设计

JavaScript 可能是迄今为止最被误解的语言,它包含的许多美妙设计被其同样包含的糟糕设计所淹没,总体上给人的印象是一种没有做好充分设计的、稍显混乱的玩具语言。现在我们再重新梳理一下语言的基本特性,仔细分辨哪些是好的、哪些是坏的。

维基百科是这样描述 JavaScript 的:

JavaScript is a high-level, dynamic, untyped, and interpreted programming language. It has been standardized in the ECMAScript language specification. Alongside HTML and CSS, it is one of the three essential technologies of World Wide Web content production; the majority of websites employ it and it is supported by all modern web browsers without plug-ins. JavaScript is prototype-based with first-class functions, making it a multi-paradigm language, supporting object-oriented, imperative, and functional programming styles. It has an API for working with text, arrays, dates and regular expressions, but does not include any I/O, such as networking, storage or graphics facilities, relying for these upon the host environment in which it is embedded.

可以清楚地看到, JavaScript 是高级的、动态的、弱类型的、解释型的语言。同时又指出它是基于原型的、以函数作为一等公民的,同时支持面向对象的、命令式的、具有函数式编程风格的多范式语言。是不是比第一印象中的强大很多?事实上,在支持多编程范式这一点上,跟 Java 、 C# 等语言相比是赢在起跑线上的。

好的设计

是对象还是函数?

Object 是类,是对象,还是函数?以一个 Java 、 C# 的程序员的视角, Object 更象一个类,在 JavaScript 中我们可以使用 new 操作符将 Object 创建出一个对象。但是很显然 Object 不是类, JavaScript 中并不存在类的概念, new 之所以能创建出对象,是因为把 Object 当做构造函数,用于对象的初始化。通过 typeof Object 操作可以知道 Object 是一个函数。再通过另外一个更科学的技巧,使用 Object.prototype.toString.apply(Object) 可以得到 "[object Function]"。下列的第一个表格列出了一些典型的考察目标,可以得出结论: Object 首先是个对象,其次它也是一个函数,或者我们可以称之为函数对象。

函数对象可以:

  • 被直接调用
  • 绑定到某个对象调用
  • 如果仅仅只有初始化对象的功能可以施加 new 构造出一个对象
  • 象普通的对象一样操作它的属性
    比如这样操作也是合法的。
Object.foo = 1;
console.log(Object.foo); // 打印出 1

顺带说一句,tyoeof 操作符是个坏设计,无法分区对象的具体类型,可以对照见下列的第二个表格中的具体示例。我们可以借助 Object.prototype.toString 或第三方库的替代方案得到真正想要的结果。

Object Function Date Array Foo
typeof ??? "function" "function" "function" "function" "function"
toString.apply(???) "[object Function]" "[object Function]" "[object Function]" "[object Function]" "[object Function]"
{} [] Math JSON
typeof ??? "object" "object" "object" "object"
toString.apply(???) "[object Object]" "[object Array]" "[object Math]" "[object JSON]"

注:
var toString = Object.prototype.toString
var Foo = function() {}

基于原型的面向对象

JavaScript 是支持面向对象编程范式的语言,跟基于类的面向对象语言(如 C++ 、 Java 、 C# 等语言)不同,Javascript 是基于原型的面向对象设计。出于某种妥协,从语法上构建一个对象跟传统的基于类的面向对象语言类似,都是通过 new 操作符创建,但是工作方式存在巨大的差别。这种使用 new 操作符创建对象的方式由于隐藏了基于原型的面向对象机制,方便传统程序员的习惯,也容易引起初学者混淆,是存在争议的。
JavaScript 对象拥有一个不对外公开的 __proto__ 属性引用到对象的原型,获取对象属性值的过程为:

  1. 首先获取对象自身属性的值
  2. 如果对象本身的属性不存在,者获取对象原型引用的属性值
  3. 依次类推,直到原型引用为空

下面的代码片段是 JavaScript 使用传统风格的面向对象示例。 Person 为构造函数,通过 new 操作符调用,返回初始化的对象; Person.prototype 定义原型,所有构造函数创建出来的对象,其 __proto__ 引用到 Person.prototype。

function Person(firstName, lastName, age, gender) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.gender = gender;
}

Person.prototype.nationality = 'China';

Person.prototype.getName = function() {
    return this.firstName + ' ' + this.lastName;
}

Person.prototype.getAge = function() {
    return this.age;
}

Person.prototype.getGender = function() {
    return this.gender;
}

function Employee(firstName, lastName, age, gender, title) {
    Person.apply(this, arguments);
    this.title = title;
}

Employee.prototype = new Person();

Employee.prototype.constructor = Employee;

Employee.prototype.getName = function() {
    return Person.prototype.getName.apply(this) + ', ' + this.title;
}

var classicalPerson = new Person('San', 'Su', 10, 'female');
console.log(classicalPerson.getName()); // San Su
var classicalEmployee = new Employee('San', 'Su', 10, 'female', 'Manager');
console.log(classicalEmployee.getName()); // San Su, Manager

再比较一下基于原型的面向对象的实现,跟上一个代码片段的功能是等价的,可以看到这种原生的实现方式简洁且同样富有表现力。

var personPrototype = {
    nationality: 'China', 
    getName: function() {
        return this.firstName + ' ' + this.lastName; 
    },
    getAge: function() {
        return this.age;
    },
    getGender: function() {
        return this.gender;
    }
};

var prototypalPerson = Object.create(personPrototype);

prototypalPerson.firstName = 'San';
prototypalPerson.lastName = 'Su';
prototypalPerson.age = 10;
prototypalPerson.gender = 'female';

prototypalPerson.getName(); // San Su

var employeePrototype = Object.create(personPrototype);
employeePrototype.getName = function() {
    return personPrototype.getName.apply(this) + ', ' + this.title;
}

employeePrototype.firstName = 'San';
employeePrototype.lastName = 'Su';
employeePrototype.age = 10;
employeePrototype.gender = 'female';
employeePrototype.title = 'Manager';

employeePrototype.getName();  // San Su, Manager

函数式特性

JavaScript 是以函数作为一等公民的语言,支持 lambda 表达式,高阶函数,柯里化等函数式特性。下面的代码片段是柯里化的实现示例,将 add 函数施加柯里化函数编程变成一个新的函数。 顺便说明一下,示例代码中使用了 arguments 对象,这个对象是一种类数组的对象,通过 Object.prototype.apply(arguments) 可以得验证,其结果是 "[object Object]",需要通过 Array.prototype.slice 函数转换成 Array , arguments 对象是一种坏的设计,需要加以注意。

function curry(fn) {
    var slice = Array.prototype.slice;
    args = slice.call(arguments, 1);
    return function () {
        return fn.apply(null, args.concat(slice.apply(arguments)));
    };
};

function add(a, b) {
    return a + b;
}

var plusOne = curry(add, 1);
plusOne(3); // 4;
curry(add, 2)(3); // 5

坏的设计

全局变量

全局变量可以说是 JavaScript 语言的万恶之源,有三种方式可以定义全局变量:

  1. 在任何函数之外定义
var foo = value;
  1. 通过全局对象的属性
window.foo = value; // 在node.js中为 global.foo = value;
  1. 直接使用没有申明的变量
foo = value; // 在node.js中为 global.foo = value;

第三种定义方式是罪魁祸首,一不小心就会定义或使用全局变量,引起难以排查的Bug。

作用域

JavaScript 使用了类似 C 语言的使用花括号的代码块设计,但是跟 C 语言不同的是代码块并不会隔离出一个单独的作用域,JavaScript 是以函数为单位创建作用域d的。这种松散的作用域设计与全局变量的设计遥相呼应,成为一对奇葩组合,祸害了一批又一批无辜的前端码农。

自动插入分号

JavaScript 解释器检测到代码行的最后如果缺少分号会企图自动补一个分号进行修正。这种设计会带来另外一个后果,先看下代码示例。getFoo 被调用之后会得到正确的对象,getFoo_undefined 被调用之后会得到 undefined 。所以,JavaScript 程序员对左花括号是否需要另起一行的终极问题上是没有争议的,我们要顺势而为。

function getFoo() {
    return {
        foo: 1
    }
}

function getFoo_undefined() {
    return
    {
        foo: 1
    }
}

undeclared 、 undefined 与 null

undeclare undefined null
描述 未申明的变量 已申明未赋值的变量 已申明赋空值的变量
typeof ??? "undefined" "undefined" "object"
toString.apply(???) 引发未定义异常 "[object Undefined]" "[object Null]"

注:
var toString = Object.prototype.toString

Number类型

虽然我们在很多时候可以肆无忌惮地写出 if (foo === 1) { /* do something */ } 这样的代码通常是没有问题的,但是我们还是需要做到心里有数,JavaScript 中的 Number 是浮点型,不存在整形甚至不是 decimal 类型。再看段代码:

var foo = (0.2 - 0.1) * 10 === 1; // foo equals true
var bar = (0.3 - 0.2) * 10 === 1; // bar equals false

结束语

可能有些同学看出来了,这篇文章根本就是 JavaScript: The Good Parts 的学习笔记啊。这本书确实是 JavaScript 领域必备图书之一,在 2008 年出版,是基于 EcmaScript 3 的标准。可以看到 EcmaScript 5 / EcmaScript 6 针对这本书上列举的痛点基本都做了改进,甚至是飞跃。了解 EcmaScript 3 的基本知识,对理解 JavaScript 语言和生态圈的演化有非常大的帮助。

出处:后端程序员的 JavaScript 之旅 - 好的设计与坏的设计

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

推荐阅读更多精彩内容