我所理解的JavaScript

JavaScript 是一种动态的,基于原型多范式的脚本语言,支持面向过程面向对象函数式编程,命令式声明式的编程风格。比如 C 风格的过程式编程,包括大括号和复杂的 for 语句,以及不太明显的 Scheme/Lisp 风格的函数式编程。

1
2
3
4
5
6
7
//命令式
var girls = [];
for(var i = 0; i < schools.length; i++){
girls.push(schools[i].girls)
}
//声明式
var girls = schools.map(s => s.girls);

JavaScript 是简单直接的,你不需要对编程有很多的了解就可以用它来完成工作。事实上,每个人的电脑里都安装了一个 JavaScript 解释器,只需要打开你的浏览器,在控制台输上一段 alert('hello world!'),你就已经在编写 JavaScript 了。

JavaScript 也是一门重要的语言,它与浏览器结合在一起,我们无时无刻不在享受 JavaScript 的魔力,从这里也能获知,JavaScript 是 GitHub 上最为流行的语言。

解释型与编译型

动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言

静态类型语言:静态类型语言的数据类型是在编译期间检查的,也就是需要声明所有变量的数据类型

强类型语言:强制数据类型定义的语言

弱类型语言:数据类型可以被忽略的语言, 一个变量可以赋不同数据类型的值

编译型语言:在编译过程中生成目标平台的指令

解释型语言:在运行过程中才生成目标平台的指令

其实,现在已经不适合用解释型和编译型来区分一门语言了,不能说一种语言只能是解释或者只能是编译。比如 C 语言可以说是编译型语言,编译生成的目标文件(机器码)是针对特定 CPU 的。而 Java 虽然是非编译型的,却也有某种编译过程,它们编译生成的通常是平台无关的中间代码(字节码.class文件),而*虚拟机( Java 虚拟机,JVM)在运行过程中将中间代码翻译成目标平台 (不同 CPU 平台) 的指令。

尽管 JavaScript 通常被认为是一门解释执行的语言,但事实上它也有编译。传统的 JavaScript 引擎是把 JavaScript 代码先编译为字节码,然后再通过解释器执行字节码。而新一代的 JavaScript 引擎为了提高性能,引入了 Java 虚拟机和 C++ 编译器中的众多技术,比如 V8 是运用 JIT (Just-in-time,即时编译) 技术,不通过解释器执行,而且直接编译成运行在 CPU 上的机器码,不过,就在几个月前, V8 新增了一个 Ignition 字节码解释器 ,将默认启动。具体可参考 V8 Ignition:JS 引擎与字节码的不解之缘

JavaScript 的优势

对象字面量

JavaScript 有非常强大的对象字面量表示法。通过列出对象的组成部分,就能创建对象。这种表示法也是 JSON 的灵感来源。

1
2
3
4
let person = {
name: 'peter',
age: 23
}

函数

函数可能是 JavaScript 中设计最出色的部分。函数是一等公民,这意味着可以把函数像其它值一样传递。 一个常见的用法是把匿名函数作为回调函数传递到异步函数中。

函数声明和函数表达式

1
2
3
4
5
6
7
8
// 函数声明
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}
// 函数表达式
foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

闭包

闭包的本质源自两点,词法作用域函数当作值传递,一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量。我们可以通过闭包模拟私用变量的特性,也可以用来实现柯里化,一种多参数函数的方法。

1
2
3
4
5
const add = R.curry((a, b) => a + b)
add(1, 2) // => 3
const add1 = add(1)
add1(2) // => 3
add1(10) // => 11

高阶函数

高阶函数是指接受或者返回一个函数的函数。因为在 JavaScript 中函数是一等公民,所以高阶函数非常容易实现。比如原生的 Array.map , Array.filter 等方法。函数当做值传递,柯里化,高阶函数等都是 JavaScript 函数式编程的体现。

1
2
let numbers = [1, 5, 10, 15];
let doubles = numbers.map( x => x ** 2);

函数重载与重写

JavaScript 不能像其他语言 (如 Java) 一样,可以为一个函数编写两个定义,只要这两个定义的签名 (接受参数的类型和数量) 不同即可。得益于动态弱类型的特性,JavaScript 可以通过检查传入函数中参数的类型和数量,作出不同的反应,来模仿函数的重载。

另外,函数重写是被 JavaScript 原生支持的,对实例对象重写函数也不会影响原型链中的函数。

原型

原型 (prototype) 继承是一个有争议的特性,JavaScript 是唯一一个被广泛使用的基于原型继承的语言,如果你尝试对 JavaScript 使用类的设计模式,将会遇到困难。比如抽象类和接口。

在 Java 等面向类的语言中,使用抽象类也被称为继承,是一种强耦合设计,用来描述 is-a 关系,是对一种事物的抽象。而接口通常用来描述对象所共有的行为,是对行为的抽象,是一种 has-a 关系,可以用来实现组合。

接口和抽象类主要有两点作用:

  • 一是通过向上转型来隐藏对象的真正类型,体现对象的多态性。
  • 二是约定对象间的一些行为。

而 JavaScript 是弱类型的,弱类型意味着不需要利用抽象类或者接口对对象做向上转型,对象继承关系也无关紧要。除了基本数据类型外的其他对象都可以天生地被向上转型成 Object 类型。可以说,JavaScript的多态性是与生俱来的

1
var duck = new Animal()

在动态类型语言的面向对象设计中,鸭子类型的概念至关重要,利用鸭子类型的思想,我们不需要借助父类就能实现面向接口编程。例如,如果一个对象有length属性,可以通过下标存取属性,那么就能当做数组来使用,在

Object.prototype.toString.call([]) === '[object Array]' 被发现之前,我们经常用下面的方式来判断对象是否是一个数组

1
2
3
4
5
6
var isArray = function(obj) {
return obj &&
typeof obj === 'object' &&
typeof obj.length === 'number' &&
typeof obj.splice === 'function'
}

面向对象 (OOP) 其实是对事物本质的抽象,而 JavaScript 实现这种抽象的方法叫原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function Foo () {
this.value = 12
}
Foo.prototype = {
val: 'val',
info: {
name: 'peter',
age: 15
},
method: function () {
console.log('this.value', this.value)
console.log('this.foo', this.foo)
}
}
function Bar () {}
Bar.prototype = new Foo()
Bar.prototype.foo = 'Hello World'
Bar.prototype.constructor = Bar
var b1 = new Bar()
var b2 = new Bar()
b1.info.name = 'jack'
b2.info.name = 'tom'
b1.foo = 'foo1'
b1.val = 'b1val'
b2.value = 'b2value'
b1.foo // foo1
b1.val // b1val
b1.info.name // tom
b1.info.age // 15
b1.method() // this.value 12 this.foo foo1
b2.foo // Hello World
b2.value // b2value
b2.info.name // tom
b2.info.age // 15
b2.method() // this.value b2value this.foo Hello World

初始值

赋值后


可以看出,当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。而对象的属性是无法修改其原型链中的同名属性,只会自身创建一个同名的属性并为其赋值。

原型继承的一个最大特性就是灵活,因为原型链上的每一个对象及原型链本身都是可以动态修改的。而类继承需要先定义好类和类之间的继承关系,而很难在运行时动态修改这些已经定义好的东西。

动态性

动态弱类型特性,动态混入,鸭子类型,动态修改原型或原型链,甚至修改原生对象的原型,这些特性都是 JavaScript 丰富表现力的源泉。Java 的一些设计模式虽然保证了代码的稳定性,但有时也禁锢了代码的灵活性。

JavaScript 的缺陷

全局变量

全局变量是在所有作用域中都可见的变量,会使得程序难以管理,不过我们也有缓解这个问题的办法,比如创建一个唯一的全局变量作为你应用的容器,或者使用闭包(内部函数总是可以访问其所在的外部函数中声明的参数和变量)来进行信息隐藏。

作用域

大多数语言都拥有块级作用域,在代码块内定义的变量在代码块外是不可见的。可喜的是,ES6新增了let关键字,它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

typeof

typeof的返回值几乎都是错误的,我们几乎不可能得到正确的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object

NaN

1
NaN === NaN // false

==

JavaScript 是弱类型语言,这就意味着,== 操作符会为了比较两个值而进行强制类型转换,而 === 操作符不会。

1
2
0 == "" // true
0 === "" // false

参考资料:

JavaScript高级程序设计
JavaScript语言精粹
你不知道的JavaScript
JavaScript设计模式与开发实践
JavaScript 秘密花园
MDN
程序的编译与解释有什么区别?
V8 引擎本用了什么编译技术,使得用 Javascript 与用 C 写出来的代码性能相差无几?