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__
属性引用到对象的原型,获取对象属性值的过程为:
- 首先获取对象自身属性的值
- 如果对象本身的属性不存在,者获取对象原型引用的属性值
- 依次类推,直到原型引用为空
下面的代码片段是 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 语言的万恶之源,有三种方式可以定义全局变量:
- 在任何函数之外定义
var foo = value;
- 通过全局对象的属性
window.foo = value; // 在node.js中为 global.foo = value;
- 直接使用没有申明的变量
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 语言和生态圈的演化有非常大的帮助。