丑陋的 JS
Posted on Sat 05 December 2020 in Journal
作为一个老程序员,长期工作在后端服务器的开发, 由于项目原因,最近会做一些前端 JavaScript 的开发,于是系统地学习了久违的 JavaScript, 第一感觉就是 Javascript 很丑陋。 当然,这是从一位老 C++ 程序员的角度来看 JavaScript , 所以觉的丑,而前端程序员可能看起来很美。
就象一开始看着总想吐,吐着吐着就习惯了,这里总结一下 JS 的槽点
1. 混乱的作用域
C++ 中有块级作用域,Javascript 中有变量提升和函数提升,其实都是提到作用域的最前面
C++的变量的作用域可能是全局的(文件级作用域),局部的(函数级或者块级作用域), 类(class)级别和名字空间(namespace)级别的。
JavaScript 呢,其实差不多,不过更加简陋,举例如下,相似的代码在 C++ 无疑报错
var color = "blue"; //global variable
//函数提升,这里可以直接调用 changeColor
changeColor();
//变量提升,background 声明已经提到最前面,只不过没有初始化
console.log("color is " + color + ", background is " + background); //olor is red, background is undefined
//没有块级作用域, background 在全局作用域中
if(true) {
var background = "brown";
}
console.log("background is " + background); //background is brown
var arr = [];
for(var i = 0; i < 10; i ++) {
arr.push(i);
}
console.log(arr); //arr conains numbers of 0 ~ 9
console.log(i); //i is 10
function changeColor() { //global function
var anotherColor = "red"; //local variable in changeColor
function swapColors() {
var tempColor = anotherColor; //local variable in swapColor
if(color !== tempColor) {
color = tempColor;
}
}
swapColors();
}
2. 函数参数随便传
C++中的函数参数需要严格定义顺序,类型和个数,不一样的顺序,类型和个数的相同名字的函数就是不相同的函数,称为函数重载。
JavaScript 就不一样了,不存在函数重载,定义的函数有三个参数,你却可以传入三个参数,五个参数,任何个数都行,举例如下
function sortArgs(a, b, c) {
return Array.from(arguments).sort(function (a, b) { return a - b; });
}
var retArr = sortArgs(5, 3, 7);
console.log(retArr);
//output: [ 3, 5, 7 ]
retArr = sortArgs(5, 3, 7, 1, 9, 8);
console.log(retArr);
//output: [ 1, 3, 5, 7, 8, 9 ]
3. 蹩脚的对象构建和继承
面向对象的基本概念之一就是类, 类定义了成员属性和方法,通过类就可创建多个具有相同属性和方法的对象。
JS 中没有类的概念,它只有对象- “对象是一个无序属性的集合,属性可以是一个基本值,一个对象,或者一个函数”。
C++ 中有构造函数,拷贝构造函数,析构函数, JS 中只有函数,new
后面跟任意一个函数,这个函数就是构造函数,而 每个函数都有一个原型对象,每个函数的原型都有一个 constructor 属性,这个属性就指向函数本身。有点绕。
定义和创建对象也挺简单,例如以下三种方法
function Book() {
this.title = "";
this.author = "";
this.edition = 1;
this.getAuthor = function() {
return this.author;
}
this.setAuthor = function(author) {
this.author = author;
}
this.toString = function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
}
function createBook(title, author, edition) {
var obj = new Object();
obj.title = title;
obj.author = author;
obj.edition = edition;
obj.getAuthor = function() {
return this.author;
}
obj.setAuthor = function(author) {
this.author = author;
}
obj.toString = function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
return obj;
}
var book0 = {
title: "reacforing",
author: "Martin",
edition: 2,
toString: function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
}
console.log("book0:", book0.toString());
var book1 = new Book();
book1.title = "test driven development";
book1.setAuthor("Kent");
console.log("book1:", book1.toString());
var book2 = new Book();
book2.title = "feature driven development";
book2.setAuthor("Unknown");
console.log("book2:", book2.toString());
var book3 = createBook("metrics driven development", "Walter", 1);
console.log("book3:", book3.toString());
上面的三个对象 book0, book1, book2, book3 各自的属性和方法都是独立的,显然效率不高,起码成员方法是可以共享的。
我们可以用继承来做到成员的共享,JavaScript 没有类,只有对象,没有类继承,只有对象继承。
一个对象 A 以另外一个对象 B 为原型,那么就可以认为 A 继承自 B。 JS中专门用来作原型的对象就好比C++中的类,原型对象的继承就好比类继承。
例如上面的 Book 函数好比 Book 类, 我们要声明一个 EBook 函数从 Book 函数继承,也就是把 EBook 的原型对象设为 Book.
这样做还不够,还需要把 EBook.prototype
的 constructor 属性改回 EBook 函数, 代码如下:
var assert = require('assert');
var book0 = {
title: "reacforing",
author: "Martin",
edition: 2,
toString: function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
}
console.log("book0:", book0.toString());
var book1 = new Book("test driven development","Kent", 4);
console.log("book1:", book1.toString());
var book2 = new Book("feature driven development","Unknown", 1);
console.log("book2:", book2.toString());
var book3 = createBook("metrics driven development", "Walter", 1);
console.log("book3:", book3.toString());
function createBook(title, author, edition) {
var obj = new Object();
obj.title = title;
obj.author = author;
obj.edition = edition;
obj.getAuthor = function() {
return this.author;
}
obj.setAuthor = function(author) {
this.author = author;
}
obj.toString = function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
return obj;
}
function Book( title, author, edition) {
this.title = title;
this.author = author;
this.edition = edition;
this.getAuthor = function() {
return this.author;
}
this.setAuthor = function(author) {
this.author = author;
}
this.toString = function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}`;
}
}
function EBook( title, author, edition, url) {
Book.call(this, title, author, edition);
this.url = url;
this.getUrl = function() {
return this.url;
}
this.setUrl = function(url) {
this.url = url;
}
this.toString = function() {
return `title=${this.title}, author=${this.author}, edition=${this.edition}, url=${this.url}`;
}
}
Book.prototype.getEdition= function() {
return this.edition;
}
console.log("book1.constructor: ",book1.constructor)
EBook.prototype = new Book();
ebook1 = new EBook("effective c++", "mayer", 3, "http://t.c");
console.log("ebook1.constructor: ",ebook1.constructor);
Object.defineProperty(EBook.prototype, "constructor", {
enumrable: false,
value: EBook,
writable: true
})
console.log("ebook1.constructor: ",ebook1.constructor);
console.log("ebook1.edition: ", ebook1.getEdition());
console.log("ebook1: ", ebook1.toString());
assert(book1 instanceof Book);
assert(ebook1 instanceof EBook);
C++ 中优雅的 "class EBook: publich Book" 在JavaScript 中搞得这么丑陋,好在 ES6中终于引入了 class
关键字,这样的定义看起来好多了
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
isAlive() {
return true;
}
static compare(user1, user2) {
return user1.age - user2.age;
}
}
var alice = new User("Alice", 20);
console.log("Alice: ", alice);
var bob = new User("Bob", 30);
console.log("Bob: ", bob);
assert(alice.isAlive());
assert(User.compare(alice, bob) < 0);
class Employee extends User {
constructor(name, age, department) {
super(name, age);
this.department = department;
}
getDepartment() {
return this.department;
}
}
var carl = new Employee("carl", 40, "QA")
console.log("carl: ", carl);
其实,这只是语法糖,底层还是基于原型链的实现
4. 数组其实是对象
C++ 在语言层面就很好地支持了数组,而JavaScript 的数组其实就是一种对象,数组元素可以是任意类型,JS 数组是动态的,它没有边界,可以根据需要增长或缩减,它可以是稀疏的,数组元素的索引可以不连续.
const users = ["alice", "bob", "carl", "david"];
users.push("elain");
users.unshift("walter");
console.log("users: ", users);
users.pop();
users.shift();
console.log("users: ", users);
users[1000] = "finance";
console.log("users length: ", users.length);
console.log("--------- users ------------");
for(var i=0, len=users.length; i < len; i++) {
if(users[i] === undefined) {
continue;
}
console.log(i, "=>", users[i]);
}
console.log("--------- departments ------------");
//array contains any type
const departments = new Array("dev", "qa", "ops", 1.0, 2, []);
console.log(departments);
//no array boundary
console.log(users[10]); //print undefined
5. 运算符混乱
首先就是严格相等与非严格相等, 所以我们尽量使用 ===
和 !==
来进行两个值的比较
var a = "10";
var b = 10
console.log(null == undefined); //true
console.log(a == b); //true
console.log(a === b); //false
逻辑运算符就更乱了,逻辑表达式返回的值并不一定是 true 或 false,而可能是任意类型
- JS 中真假值的判断
true
:. 对象、非零数字、非空字符串-
false
: 0、""(空字符串)、null、false、undefined、NaN -
JS中的短路求值
-
a&&b
:左操作数为假值时,返回左操作数,否则返回右操作数。 -a||b
:左操作数为假值时,返回右操作数,否则返回左操作数。 -
通过
!!
把一个其他类型的变量转成的 bool 类型 -
通过
+
把一个其他类型的变量转成的 number 类型
举例如下:
var a = "10";
var b = 10;
var c = null;
console.log(c == undefined); //true
console.log(a == b); //true
console.log(a === b); //false
console.log(typeof a);//string
console.log(typeof !!a, !!a)//boolean, true
console.log(typeof +a, +a)//number, 10
console.log(c || 20);//20
console.log(c && 20);//null
6. 蹩脚的封装
面向对象最重要的特性可能就是封装了,C++ 中有 public, protected 和 private 三种可见性,而 JS 呢,全都没有,只能通过代理或闭包这种比较麻烦的方式来封装隐藏私有成员。
闭包是指有权访问另一个函数作用域中的变量的函数。 创建闭包的常见方式是在一个函数内部创建另一个函数。
function Task() {
let priority;
this.getPriority = () => priority;
this.setPriority = value => { priority = value;};
}
const task = new Task();
task.setPriority(1);
console.log(task.getPriority());
7. 回调地狱
JS 的函数回调往往会搞成下面这个样子,一层一层回调下去,超过三层就让人头大了, 例如:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
好在 ES6 有了 promise , 情况会好很多
我们可以在支持 ES6 的浏览器中改写为
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
改成 lambda 表达式样的箭头函数看起来就更加清晰了
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
参见 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
8. 全局变量泛滥
在 JS 中, 不在函数内部的变量都是全局的,全局变量泛滥会造成许多问题, 解决的方法是减少全局变量的使用,整个应用可以只用一个唯一的全局变量。
var MyApp = {}
MyApp.project = {
"count":0,
"tasks": {}
}
Java 有包 package 的概念, C++ 中有命名空间 namespace, JavaScript 呢?
可能对应的就是模块了, 就用一个全局变量作为模块的命名空间, 用法如下
var moduleName = (function(){
//privateVariable;
//priviateMethods;
return {
//publicMethods
};
})();
将公有函数放在一个对象字面量返回, 赋给代表这个模块的全局变量,这样你只能用它的公有方法, 私有方法和变量是不可见的.
9. 易错的 with
使用 with 的效果可能不可预料,所以最好别用
with(obj) {
a = b;
}
//它有可能是下面四种情况
a = b;
a = obj.b;
obj.a = b;;
obj.a = obj.b;
10. 邪恶的 eval
eval 看起来很好用,可是却很危险,搞不好就会变得 "evil" (邪恶的)
foo = 2;
eval('foo = foo + 2; console.log(foo);'); //print 4
邪恶的原因在于 1. 性能原因: 由于 eval 会用到 JS 的解释/编译功能, 性能会差很多 2. 注入的可能: 类似于 SQL 注入, eval 的内容也可被注入