我所理解的 JavaScript

690 阅读8分钟
原文链接: github.com

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

//命令式
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 的灵感来源。

let person = {
  name: 'peter',
  age: 23
}

函数

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

函数声明和函数表达式

// 函数声明
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

// 函数表达式
foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

闭包

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

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 函数式编程的体现。

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的多态性是与生俱来的

var duck = new Animal()

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

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

var isArray = function(obj) {
    return obj && 
      typeof obj === 'object' && 
      typeof obj.length === 'number' && 
      typeof obj.splice === 'function'
}

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

   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的返回值几乎都是错误的,我们几乎不可能得到正确的结果。

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

NaN === NaN // false

==

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

0 == "" // true
0 === "" // false

参考资料:

JavaScript高级程序设计

JavaScript语言精粹

你不知道的JavaScript

JavaScript设计模式与开发实践

JavaScript 秘密花园

MDN

程序的编译与解释有什么区别?

V8 引擎本用了什么编译技术,使得用 Javascript 与用 C 写出来的代码性能相差无几?