# JavaScript
JavaScript 中的一些概念和其它语言完全不同。
- 函数式:函数是一等公民、高阶函数、闭包
- 函数作用域、块级作用域
- 基于原型的面向对象
- 函数、数组也是对象
# 编码、运行
# 运行时环境
JavaScript 语言一直在发展,但 JS 引擎的支持往往没那么快,可以在 https://kangax.github.io/compat-table/es6/ (opens new window) 查看 JS 特性的支持情况。对于不支持的特性可以使用 Babel (opens new window) 或 Traceur 转换。
JavaScript 可以运行在各种环境中,但最常见的还是运行在浏览器和 Node 上。在现实世界中浏览器并不能完美实现 Web 标准,这就引入了 Polyfill/Shim 的概念。Babel 也提供了 Polyfill 的 runetime。
浏览器和 Node 是不同的宿主环境,一些项目用 Node.js 模拟了一套浏览器环境。
# 编译
# 作用域
var
的声明提升和函数作用域这两个特性和其它语言差异大,迷惑了许多开发者。ES6 的 let
和 const
解决了这个问题,采用了块级作用域、没有声明提升的特性。
# 类型
JS 有 7 种类型:Undefined、Null、Boolean、String、Number、Symbol、Object。Symbol 是 ES6 新增的。整数和浮点数都是 Number。函数、数组的本质都是 Object。
typeof 用于判断类型。
typeof null === 'object'
是一个坑typeof [] === 'object'
因为数组本质是对象typeof foo === 'function'
函数虽然本质也是对象,但 typeof 返回的是 function
# 类型转换
==
运算涉及隐式类型转换,JS 有一套相当复杂难懂的转换规则。此外 if
、逻辑运算、+
也会涉及到隐式类型转换。
# 装箱、拆箱
Number、String 都是值类型,但有了自动装箱机制,我们可以:
(3.5).toFixed()
"hello".substring(1)
同样的自动拆箱机制:
(new Number(0.1)) + 0.2
# 函数
精通 JavaScript 的一个秘诀是要明白 JS 函数和其它语言存在巨大差异:
- 函数也是对象,
foo instanceof Function
可以证明这点,也有人用foo.xxx = 1
实现一些高级的需求 - 函数会有 2 个隐含参数:
this
、arguments
精通 JavaScript 的另一个秘诀是要将它视作一门函数式语言,这本身是个大话题,可以阅读《JavaScript函数式编程指南》 (opens new window)。这里简单列几个术语吧:
- 柯里化
- TODO:这本书我自己也没看,等看过再说
# 函数表达式和函数声明
函数表达式可以立即调用 (IIFE),但函数声明不可以。为了让 JS 引擎能在语法上区分二者,我们有几种办法:
- 加括号:
(function(){ ... })(a, b)
- 加括号:
(function(){ ... }(a, b))
- 在前面加任意一元操作符:
+function(){ ... }(a, b)
- 这样写会识别为函数声明,导致报错:
function(){ ... }(a, b)
IIFE (opens new window) 是非常基础的技巧,需要熟练掌握。
# this
在其它语言中,this
的指向取决于函数定义的位置。但 JavaScript 把 this
看做是一种入参,所以 this
的指向取决于函数被调用的位置。共有这几种情况:
- 作为函数 (function) 直接调用,这时候指向 window 对象,但开启
'use strict'
之后指向 undefined - 作为对象的方法 (method) 调用,指向相应的对象
- 作为构造器,通过
new
调用,指向new
创建出来的新对象 - 通过
apply()
或者call()
调用可以指定this
的指向 - 通过
bind()
创建函数是一种柯里化技术,它会提前传入this
参数 - ES6 箭头函数中
this
指的是一个变量,而不是一个入参,所以会按照词法作用域的规则来确定其指向
手写 bind
Function.prototype.mybind = function () {
var args = Array.prototype.slice.call(arguments);
var that = args.shift();
var func = this;
return function () {
func.apply(that, args);
}
}
手写 new
Function.prototype.new = function () {
var obj = Object.create(this.prototype);
var ret = this.apply(obj, arguments);
return ret || obj;
}
var arr = Array.new(3);
# 原型 & 面向对象
JS 是基于原型的语言,但可以实现面向对象的三大特性:
- 封装
- 使用原型链可以实现继承
- 动态语言的鸭式辨形法天然支持多态
基于原型的语言在使用上和传统 OOP 有很大不同,为了让它看起来像传统 OOP,JS 引入了下面这些东西:
- 函数的
prototype
属性指向一个原型,这个原型的constructor
指回了这个函数;因此我们一定有func.prototype.constructor === func
,除非你想实现一些鬼才操作 new
和instanceof
都是语法糖
ES6 引入的 Class 语法,也是语法糖。
# 原型链
原型链在 JS 语言规范里面叫做 [[Prototype]]
,规范并没有要求实现这个字段。但实际上所有浏览器、Node.js 都实现了,而且都叫做 __proto__
。
“读”操作会顺着原型链一直找,“写”操作只会写当前对象不会覆盖原型链上的对象。
# ES5:基于原型的面向对象
function Shape(x, y) {
this.x = x;
this.y = y;
}
Shape.prototype = {
__proto__: Object.prototype,
constructor: Shape,
getLocation: function () {
return '(' + this.x + ', ' + this.y + ')';
}
};
function Rectangle(x, y, width, height) {
Shape.call(this, x, y);
this.width = width;
this.height = height;
}
Rectangle.prototype = {
__proto__: Shape.prototype,
constructor: Rectangle,
getArea: function () {
return this.width * this.height;
}
};
var rect = new Rectangle(1, 1, 3, 4);
console.log(rect.getLocation());
console.log(rect.getArea());
# ES6:面向对象
class Shape {
constructor(x, y) {
this.x = x;
this.y = y;
}
getLocation() {
return `(${this.x}, ${this.y})`;
}
}
class Rectangle extends Shape {
constructor(x, y, width, height) {
super(x, y);
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
var rect = new Rectangle(1, 1, 3, 4);
console.log(rect.getLocation());
console.log(rect.getArea());
# 程序控制流
# Promise
# Generator
使用 Generator 封装 Flat 操作
// 使用 Generator 封装数组 Flat 操作
// Flat 就是把嵌套数组“拍平”成一维的,算法就是把数组当做一棵树进行 DFS 遍历
function* flat(arr) {
for (let i = 0; i < arr.length; i++) {
let a = arr[i];
if (Array.isArray(a)) {
yield* flat(a);
} else {
yield a;
}
}
}
let input = [1, [[2, 3], 4], [5, 6]];
for (var a of flat(input)) {
console.log(a); // 1 2 3 4 5 6
}
let output = Array.from(flat(input));
console.log(output); // 1 2 3 4 5 6
也可以用 Generator 生成 Iterator。
Generator 和 Iterator 的关系
// 替换 Array 默认的迭代器 (Iterator)
// 实现从两边向中间的顺序遍历
let arr = [1, 2, 3, 4, 5];
// for (let i of arr) {
// console.log(i); // 1 2 3 4 5
// }
Array.prototype[Symbol.iterator] = function* () {
if (this.length === 0) return;
let l = 0;
let r = this.length - 1;
while (l < r) {
yield this[l];
yield this[r];
l += 1;
r -= 1;
}
yield this[l];
}
for (let i of arr) {
console.log(i); // 1 5 2 4 3
}