基础

2021/03/08

# 内置类型

JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。

基本类型有六种: null, undefined, boolean, number, string, symbol

其中 JS 的数字类型是浮点类型的,没有整型。并且浮点类型基于 IEEE754 标准实现,在使用中会遇到某些 Bug。NaN 也属于 number 类型,并且 NaN 不等于自身。

对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型:

let a = 111; // 这只是字面量,不是 number 类型
a.toString(); // 使用时候才会转换为对象类型

对象(Object)是引用类型,在使用过程中会遇到浅拷贝和深拷贝的问题:

let a = { name: 'FE' };
let b = a;
b.name = 'EF';
console.log(a.name) // EF

# Typeof

typeof 对于基本类型,除了 null 都可以显示正确的类型:

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeof 对于对象,除了函数都会显示 object

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

# 类型转换

# 转Boolean

在条件判断时,除了 undefined, null, false, NaN, '', 0, -0, 其他所有值都转为 true,包括所有对象。

# 对象转基本类型

对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。

let a = {
  valueOf() {
    return 0
  }
}

当然你也可以重写 Symbol.toPrimitive,该方法在转基本类型时调用优先级最高。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
1 + a // => 3
'1' + a // => '12'

# 四则运算符

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

对于加号需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1

# == 操作符

比较运算 x == y,其中 xy 是值,产生 ture 或者 false。这样的比较按如下方式进行:

  1. 若 Type(x) 与 Type(y) 相同,则: a. 若 Type(x) 为 Undefined,返回 true b. 若 Type(x) 为 Null,返回 true c. 若 Type(x) 为 Number,则: i. 若 x 为 null,返回 false ii: 若 y 为 null,返回 false iii: 若 x 与 y 为相等数值,返回 true iv: 若 x 为 +0,y 为 -0,返回 true v: 若 x 为 -0,y 为 +0,返回 true vi: 其他返回 false d: 若 Type(x) 为 String,则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。否则返回 false e: 若 Type(x) 为 Boolean,当 x 和 y 同为 true 或同为 false 时返回 true。否则,返回 false f: 当 x 和 y 为引用同一对象时返回 true。否则,返回 false
  2. 若 x 为 null 且 y 为 undefined,返回 true
  3. 若 x 为 undefined 且 y 为 null,返回 true
  4. 若 Type(x) 为 Number 且 Type(y) 为 String,返回 x == ToNumber(y) 的结果
  5. 若 Type(x) 为 String 且 Type(y) 为 Number,返回 ToNumber(x) == y 的结果
  6. 若 Type(x) 为 Boolean,返回比较 ToNumber(x) == y 的结果
  7. 若 Type(y) 为 Boolean,返回比较 x == ToNumber(y) 的结果
  8. 若 Type(x) 为 StringNumber,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果
  9. 若 Type(x) 为 Object,且 Type(y) 为 StringNumber,返回比较 ToPrimitive(x) == y 的结果
  10. 返回 true

上图中的 toPrimitive 就是对象转基本类型。

这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为何为 true 的步骤:

// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true

# 比较运算符

如果是对象,就通过 toPrimitive 转换对象 如果是字符串,就通过 unicode 字符索引来比较

# 原型

# 原型链

每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。

对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

构造函数、原型和实例的关系:

  1. 对象是通过函数创建的,函数是对象 Object.__proto__ === Function.prototype, Function.__proto__ === Function.prototype
  2. 每个函数都有 prototype 属性,除了用这个方法创建的函数 let fun = Function.prototype.bind()
  3. 每个对象都有一个隐藏属性 __proto__,这个属性指向了创建这个对象的构造函数的原型(这是一个非标准属性,不可以用于编程,是供浏览器自己使用的)
  4. prototype 属性里面默认有一个 constructor 属性,该属性指向原型对象所属的构造函数

prototype_proto_ 的关系:

  1. prototype 是构造函数的属性
  2. __proto__ 是实例对象的属性

这两者都指向同一个对象

属性搜索:

  1. 在访问对象的某个成员的时候会先在对象中查找是否存在
  2. 如果当前对象中没有就在构造函数的原型对象中找
  3. 如果构造函数的原型对象中没有找到就到原型对象的原型上找
  4. 直到 Object 的原型对象的原型是 null 为止

# new 的过程

  1. 声明一个中间对象(新生成一个对象)
  2. 将该中间对象的原型指向构造函数的原型(链接到原型)
  3. 将构造函数的 this,指向该中间对象(绑定 this
  4. 返回该中间对象,即返回实例对象(返回新对象)

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

function xnew() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()

# instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function Car(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
}

const auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car); // expected output: true
console.log(auto instanceof Object); // expected output: true

我们也可以试着实现一下 instanceof

function instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype;
    // 获得对象的原型
    left = left.__proto__;
    // 判断对象的类型是否等于类型的原型
    while (true) {
        if (left === null) return false;
        if (prototype === left) return true;

        left = left.__proto__;
    }
}

# 参考资料

# this 关键字

在 Javascript 里,函数被调用的时候,除了接受声明时定义的形式参数,每一个函数还接受两个附加的参数: thisarguments。随着函数使用场合的不同,this 的值会发生变化。但是有一个总的原则,那就是 this 指的是,调用函数的那个对象。

function foo() {
	console.log(this.a);
}
var a = 1;
foo();

var obj = {
	a: 2,
	foo: foo
}
obj.foo();

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo();
c.a = 3;
console.log(c.a);

// 还有种就是利用 call, apply, bind 改变 this,这个优先级仅次于 new

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

function a() {
    return () => {
        return () => {
        	console.log(this);
        }
    }
}
console.log(a()()());  // window

箭头函数和普通函数之间有一个重要的差别 - 箭头函数没有自己的 this 值,其 this 值是继承外域的 this 值,默认指向定义它时所处的对象(宿主对象),而不是执行时的对象。所以箭头函数不仅仅是从外观上简化了函数的写法,更解决了普通函数中 this 的 hack 问题。

# 箭头函数和普通函数的区别

  • 关于 this
    • 箭头函数没有自己的 this 值,其 this 值指向外围作用域
    • 箭头函数的 this 是声明时确定,不可变;普通函数是运行时确定并且可改变(call, apply, bind
    • 箭头函数不可以当做构造函数,也就是说不可以使用 new 命令
  • 关于 arguments:箭头函数中没有 arguments 对象,若要使用,可用 rest 参数代替
  • 关于 yield:箭头函数不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  • 关于 prototype:箭头函数没有 prototype 属性

# 参考资料

# 闭包

闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

function A() {
    let a = 1;
    function B() {
        console.log(a);
    }
    return B;
}

闭包的应用场景:

  1. 使用闭包可以在 JavaScript 中模拟块级作用域;
  2. 闭包可以用于在对象中创建私有变量。
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i); // 5,5,5,5,5,5
    }, 1000);
}

上面这段代码中,for 循环中的 i 变量被内部循环的函数使用,只能获得该变量最后保留的值。也就是说,闭包中所记录的自由变量,只是对这个变量的一个引用,而非变量的值,当这个变量被改变了,闭包里获取到的变量值,也会被改变。

解决的方法之一,是让内部函数在循环创建的时候立即执行,并且捕捉当前的索引值,然后记录在自己的一个本地变量里,然后利用返回函数的方法,重写内部函数,让下一次调用的时候,返回本地变量的值。

如果我们约定,用箭头 -> 表示其前后的两次输出之间有 1 秒的时间间隔,而逗号 , 表示其前后的两次输出之间的时间间隔可以忽略,代码运行结果 5 -> 0,1,2,3,4 有如下解法:

// 解法一
for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j); // 0,1,2,3,4
        }, 1000);
    })(i);
}

console.log(new Date, i); // 5

// 解法二
var output = function (i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    output(i);  // 这里传过去的 i 值被复制了
}

console.log(new Date, i);

// 解法三(利用 ES6 let 块级作用域)
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);

如果期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,该怎么改造代码?

// 解法一
for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000 * j));  // 这里修改 0~4 的定时器时间
    })(i);
}

setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
    console.log(new Date, i);
}, 1000 * i);

// 解法二
const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});

还可以利用 ES7 中的 async/await 特性来实现:

// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();

# 模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化:

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

# CommonJS

CommonJs 是 Node 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代码中,module.exportsexports 很容易混淆,让我们来看看大致内部实现:

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// 基本实现
var module = {
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};

再来说说 module.exportsexports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

对于 CommonJS 和 ES6 中的模块化的两者区别是:

  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • 后者会编译成 require/exports 来执行的

# AMD

AMD 是由 RequireJS 提出的:

// AMD
define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
})
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
})

# CMD

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出:

define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    // 此处略去 100 行
    var b = require('./b')
    // 依赖可以就近书写
    b.doSomething()
    // ...
});

# 参考资料

# 正则表达式

# 元字符

元字符 作用
. 匹配任意字符除了换行符和回车符
[] 匹配方括号内的任意字符。比如 [0-9] 就可以用来匹配任意数字
^ ^9,这样使用代表匹配以 9 开头。[^9],这样使用代表不匹配方括号内除了 9 的字符
{1, 2} 匹配 1 到 2 位字符
(yck) 只匹配和 yck 相同字符串
\| 匹配 \| 前后任意字符
\ 转义
* 只匹配出现 0 次及以上 * 前的字符
+ 只匹配出现 1 次及以上 + 前的字符
? ? 之前字符可选

# 修饰语

修饰语 作用
i 忽略大小写
g 全局搜索
m 多行

# 字符简写

简写 作用
\w 匹配字母数字或下划线
\W 和上面相反
\s 匹配任意的空白符
\S 和上面相反
\d 匹配数字
\D 和上面相反
\b 匹配单词的开始或结束
\B 和上面相反

# 先行断言和后行断言

几个概念:

  • 零宽:只匹配位置,在匹配过程中,不占用字符,所以被称为零宽
  • 先行:正则引擎在扫描字符的时候,从左往右连续扫描字符串中的字符,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行
  • 后行:引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行,即产生回溯
  • 正向:即匹配括号中的表达式
  • 负向:不匹配括号中的表达式

正则表达式的先行断言和后行断言一共有4种形式:

  • (?=pattern) 零宽正向先行断言:某位置后面紧接着的字符序列要匹配 pattern
  • (?!pattern) 零宽负向先行断言:某位置后面紧接着的字符序列不能匹配 pattern
  • (?<=pattern) 零宽正向后行断言:某位置前面紧接着的字符序列要匹配 pattern
  • (?<!pattern) 零宽负向后行断言:某位置前面紧接着的字符序列不能匹配 pattern

如同 ^ 代表开头,$ 代表结尾,\b 代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为零宽。所谓位置,是指字符串中(每行)第一个字符的左边、最后一个字符的右边以及相邻字符的中间(假设文字方向是头左尾右)。

es5 就支持了先行断言,es2018 才支持后行断言 先行断言和后行断言某种程度上就好比使用 if 语句对匹配的字符前后做判断验证

正则解析:

/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/

此正则表达式将执行以下规则:

  • 至少一个大写英文字母,(?=.*?[A-Z])
  • 至少一个小写英文字母, (?=.*?[a-z])
  • 至少一位数字, (?=.*?[0-9])
  • 至少一个特殊字符, (?=.*?[#?!@$%^&*-])
  • 最小长度为八 .{8,}(带锚)
  • .*? 表示匹配模式是非贪婪的

? 字符紧跟在任何一个其他限制符(*, +, ?, {n}, {n,}, {n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 'oooo',o+? 将匹配单个 'o',而o+ 将匹配所有 'o'。

上次更新: 2021/5/6 下午4:08:15