String
js 中的字符串和 java 字符串都是不可变的,不同点是: java 里的字符串,如果它不是 new 出来的,会被放在常量池里,而 js 里的字符串都是放在堆中。
var str = 'abc';
str[0] = 'x';
str.length = 0;
console.log(str.length); // 3
console.log(str[0]); // a
模板字面量标签函数(ES6)
什么是“模板字面量” ?(用过但不知道叫啥…)
var str = `abc${'d'}`;
console.log(str); // abcd
var a = {
toString() { return '哇哈哈'; }
}
console.log(`我爱喝${a}`); // 我爱喝哇哈哈
模板字面量标签函数
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(typeof strings) // object
console.log(...arguments); // [ 'gap1', 'gap2', 'gap3', 'gap4' ] 1 2 3
console.log(strings.raw) // [ 'gap1', 'gap2', 'gap3', 'gap4' ]
return sumExpression;
}
var a = 1, b = 2;
var result = simpleTag`gap1${a}gap2${b}gap3${a + b}gap4`;
console.log(result); // 3
String.raw(ES6 原始字符串)
String.raw 是模板字面量标签函数的应用,它不会对要转义的字符进行转义,而是保留原始值。
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
Symbol.for (全局注册表)
Symbol.for 函数使用一个字符串作为建生成一个符号并注册在全局中,如果再次使用相同的建生成符号,会在全局中找到之前使用这个建生成的符号并返回。
let symbol1 = Symbol('a');
let symbol2 = Symbol('a');
console.log(symbol1 === symbol2); // false
// 全局符号注册表 Symbol.for
let symbol3 = Symbol.for('a');
let symbol4 = Symbol.for('a');
console.log(symbol3 === symbol4); // true
// 获取符号的“键”
let s3key = Symbol.keyFor(symbol3);
console.log(typeof s3key); // string
console.log(s3key); // a
使用符号作为属性
可以使用 Symbol 函数生成的字符串作为对象的键,如 Symbol(‘a’),这时键为 “Symbol(a)” 。
const a = {
'a': 1,
'b': 2,
[Symbol('a')]: 3, // 因这里的 Symbol(x) 生成的建都是字符串!
[Symbol('b')]: 4
};
console.log(Object.getOwnPropertyNames(a)); // [ 'a', 'b' ]
console.log(Object.getOwnPropertySymbols(a)); // [ Symbol(a), Symbol(b) ]
/**
* {
a: { value: 1, writable: true, enumerable: true, configurable: true },
b: { value: 2, writable: true, enumerable: true, configurable: true },
[Symbol(a)]: { value: 3, writable: true, enumerable: true, configurable: true },
[Symbol(b)]: { value: 4, writable: true, enumerable: true, configurable: true }
}
*/
console.log(Object.getOwnPropertyDescriptors(a));
console.log(Reflect.ownKeys(a)); // [ 'a', 'b', Symbol(a), Symbol(b) ]
生成器函数
生成器函数并不会阻塞主线程的运行,只有当 yield 有了结果(即执行一次 next()后,yield就会有结果),才会执行下一步代码。这里其实是一个语法糖,其实还是将下一步代码放到回调里去执行。
require('colors');
/**
* 生成器函数
*/
function* generator() {
console.log('开始执行 *generator 的代码'.yellow)
yield(console.log('到达第 1 个 yield'), 1);
yield(console.log('到达第 2 个 yield'), 2);
yield(console.log('到达第 3 个 yield'), 3);
console.log('*generator 的代码执行结束'.yellow)
}
const gen = generator();
console.log('得到的生成器对象:', gen);
console.log('\r\n执行第 1 个 next 方法');
console.log('第 1 个 next 方法的返回结果:', gen.next());
console.log('\r\n执行第 2 个 next 方法');
console.log('第 2 个 next 方法的返回结果:', gen.next());
console.log('\r\n执行第 3 个 next 方法');
console.log('第 3 个 next 方法的返回结果:', gen.next());
console.log('\r\n执行第 4 个 next 方法');
console.log('第 4 个 next 方法的返回结果:', gen.next());
其中, next 方法可以传参,这个参数会替换为上一个 yield 的结果值!改一下上面的代码,实践一下。
require('colors');
/**
* 生成器函数
*/
function* generator(x) {
console.log('开始执行 *generator 的代码'.yellow)
const v1 = yield(console.log('到达第 1 个 yield'), 1 + x);
const v2 = yield(console.log('到达第 2 个 yield'), 2 + v1);
const v3 = yield(console.log('到达第 3 个 yield'), 3 + v2);
console.log(v3);
console.log('*generator 的代码执行结束'.yellow)
}
const gen = generator();
console.log('得到的生成器对象:', gen);
console.log('\r\n执行第 1 个 next 方法');
console.log('第 1 个 next 方法的返回结果:', gen.next(4));
console.log('\r\n执行第 2 个 next 方法');
console.log('第 2 个 next 方法的返回结果:', gen.next(3));
console.log('\r\n执行第 3 个 next 方法');
console.log('第 3 个 next 方法的返回结果:', gen.next(2));
console.log('\r\n执行第 4 个 next 方法');
console.log('第 4 个 next 方法的返回结果:', gen.next(1));
第 1 个 next 方法传参无效,在上面这个例子中,应该要向生成器函数传参,来实现修改第 1 个 yield 的结果值!
异步迭代器(Symbol.asyncIterator)(ES9/ES2018)
异步迭代器为 for-await-of 服务,我们迭代某个可迭代对象的时候,其实就是在调用 next() 方法,它可以显示地调用,也可以隐式的调用,下面是一个隐式调用。
class Iterator {
constructor(len = 5) {
this.length = len;
}
// 异步迭代生成器函数
async *[Symbol.asyncIterator]() {
while (this.length) {
yield new Promise(resolve => {
setTimeout(() => {
resolve(--this.length)
}, 1000);
})
}
}
}
async function run() {
const iterator = new Iterator();
for await (const item of iterator) {
console.log(item);
}
}
run(); // 4 3 2 1 0
Symbol.hasInstance
在 ES6 中,instanceof 操作符会使用 Symbol.hasInanceof 来判读实例所属关系。但是它的操作数位置和 instanceof 的操作数位置相反。
class Parent {}
class Child extends Parent {
static[Symbol.hasInstance]() {
return false;
}
}
const p = new Parent();
const c = new Child();
console.log(p instanceof Parent); // true
console.log(c instanceof Parent); // true
console.log(c instanceof Child); // falses
console.log(Parent[Symbol.hasInstance](p)); // true
console.log(Parent[Symbol.hasInstance](c)); // true
console.log(Child[Symbol.hasInstance](c)); // false
Symbol.iterator
使用 for-of 的时候,如果定义了同步迭代器 Symbol.iterator,那么就会使用它。
class Emitter {
constructor(max = 5) {
this.max = max;
this.index = 0;
};
* [Symbol.iterator]() {
while (this.index < this.max) {
yield this.index++;
}
}
}
const emitter = new Emitter();
for (const iterator of emitter) {
console.log(iterator); // 0 1 2 3 4
}
Symbol.toStringTag
除了直接修改对象实例的 toString方法外,这个方法也可以重写实例对象的 toString 方法,但是它的优先级没有直接修改 toString 方法高。注意:这个方法只能在对象实例上添加,添加到它的原型对象上或构造函数上是不生效的!
class Test {
constructor() {
this[Symbol.toStringTag] = 'Module';
}
}
const test = new Test();
console.log(test); // Test { [Symbol(Symbol.toStringTag)]: 'Module' }
console.log(test.toString()); // [object Module]
测试优先级。
class Test {
constructor() {
this.toString = () => 'MyModule';
this[Symbol.toStringTag] = 'Module';
}
}
const test = new Test();
console.log(test); // Test { toString: [Function (anonymous)], [Symbol(Symbol.toStringTag)]: 'Module' }
console.log(test.toString()); // MyModule
关于 Symbol 上的函数还有很多,上面是常见的几个。
后缀递增递减
let a = 1;
const b = a++ + 0;
console.log(b); // 1
console.log(a); // 2
位操作之位移
MCMAScript 中的所有数值都以 IEEE 754 64 位格式存储(另外它还有 32 位的格式),但是前边儿又说浮点数占用的存储空间是整数的两倍(暂时标记一下,后面再找答案…)。
位操作并不直接应用到 64 位,而是先把值转为 32 位整数,然后再进行位操作!
算术位移
算术位移指的是保留符号位进行左移或右移,即符号位固定不移动,所以位移后数值的正负是不变的。
-2 >> 1 = -1
得到 -1 后,再右移会发现得到的还是 -1 。
逻辑位移
逻辑位移跟算术位移的区别就是逻辑位移会移动符号位,可能导致符号位会丢失。
- 正数无符号右移,结果对符号位无影响;
- 负数无符号右移,逻辑右移高位补 0 ,结果值会变成正数,对结果有影响;
- 假如有正负数无符号左移,则符号位会变成紧靠它右边的那一位,对结果可能都会有影响。
为什么没有 “无符号左移” 而有 “无符号右移” ?
对正数和负数进行无符号左移,可能都会改变其符号,从而得到不正确的数值;
负数无符号右移虽然得不到正确的数值,但它可以用来做哈希运算和加密等等。
布尔操作符
console.log(!NaN); // true
console.log(!!NaN); // false
关系操作符
// 字符串与字符串比较 "abc" >= "ABC" true
console.log('字符串与字符串比较 "abc" >= "ABC"', 'abc'>='ABC');
// 数值与数值比较 1 >= 1 true
console.log('数值与数值比较 1 >= 1 ', 1>=1);
// 数值与布尔比较 1 > false true
console.log('数值与布尔比较 1 > false', 1>false);
// 数值与对象比较 1 >= {} false
console.log('数值与对象比较 1 >= {}', 1>={});
// 数值与数组比较 1 >= [] true
console.log('数值与数组比较 1 >= []', 1>=[]);
// 数值与日期比较 1 >= new Date() false
console.log('数值与日期比较 1 >= new Date()', 1>=new Date());
// 数值与数值字符串比较 1 >= "1" true
console.log('数值与数值字符串比较 1 >= "1"', 1>='1');
// 数值与非数字字符串比较 1 >= "a" false
console.log('数值与非数字字符串比较 1 >= "a"', 1>='a');
// 数值与 undefined 比较 1 >= undefined false
console.log('数值与 undefined 比较 1 >= undefined', 1 >= undefined);
// 数值与 null 比较 1 >= null true
console.log('数值与 null 比较 1 >= null', 1 >= null);
// 对象与布尔比较 {valueOf() {return 0}} <= true true
console.log('对象与布尔比较 {valueOf() {return 0}} <= true', {valueOf() {return 0}} <= true);
// 对象与布尔比较 {toString() {return "0"}} <= true true
console.log('对象与布尔比较 {toString() {return "0"}} <= true', {toString() {return '0'}} <= true);
// 对象与数值比较 1 >= {valueOf() {return 0}} true
console.log('对象与数值比较 1 >= {valueOf() {return 0}}', 1 >= {valueOf() {return 0}});
// 对象与数值比较
console.log('对象与数值比较 1 >= {toString() {return "0"}}', 1 >= {toString() {return '0'}});
// 对象与对象比较 {valueOf() {return 0}} < {toString() {return "1"}} true
console.log('对象与对象比较 {valueOf() {return 0}} < {toString() {return "1"}}', {valueOf() {return 0}} < {toString() {return "1"}})
总结:
- 如果两个操作数都是字符串,则逐个比较字符序列对应的编码,如果相等则继续比较下一个;
- 如果两个操作数都是数值,则直接进行比较;
- 如果操作数有一个是数值,则将另一个操作数也转为数值;
- 如果操作数有一个是布尔值,则将布尔值转为数值后按前面的规则比较,即都变为数值后比较;
- 如果操作数有一个是对象,则先调用 valueOf 方法,取结果后根据以上规则比较,如果没有 valueOf 方法,则调用 toString 方法,取结果后根据以上规则比较。
备注:这里转为数字的操作其实是调用了 Number 方法。
标签语句
和 goto 语句有些类型,不过这里是用来跳出或跳过循环的,配合 continue 和 break 使用。
let num = 0;
xxxx: for (var i = 0; i < 10; i++) {
if (i === 5) {
break xxxx;
}
num++;
}
console.log(num); // 5
let sum = 0;
yyy: for (var i = 0; i < 11; i++) {
for (let j = 0; j < 10; j++) {
sum++;
if (j % 5 === 0) {
continue yyy;
}
}
}
console.log(sum); // 11
switch case
switch(true) {
case 1<2: console.log('1<2'); // 1<2
case false: console.log('false'); break; // false
case true: console.log('true');
}
let i = 0;
switch(i) {
case 0+0: console.log('0+0'); // 0+0
case 2: console.log('2'); // 2
}
case 后面可以跟语句,当某个条件匹配后需要使用 break ,否则会得到不正确的结果!