丑陋的 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

我们可以在支持 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 的内容也可被注入