interview - es6-10
新特性
String
新方法
String.fromCodePoint()
: 弥补String.fromCharCode()
(从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF
的字符), 识别全部 Unicode, 和codePointAt()
互反;String.raw()
: 不对\n
等转义字符转义(如换行等), 原样返回
unicode
'\u0061' // 'a'
'\u{6F}' // 'o'
"\u{20BB7}" // "𠮷"
let hello = 123; hell\u{6F}===123 // !!!
'\u{1F680}'==='\uD83D\uDE80'
'z'==='\z' ==='\172'==='\x7A'==='\u007A'==='\u{7A}'
字符串遍历 (可识别 UTF-16, for loop 不可以)
for (let codePoint of 'foo') {
console.log(codePoint) // "f", "o", "o"
}
原型方法 String.raw()
和 UTF-16 有关的新方法 codePointAt(), normalize()
新方法 includes(), startsWith(), endsWith(), repeat(), padStart(), padEnd(), trimStart(), trimEnd(), matchAll()
Object
Object.freeze / Object.isFrozen
Object.seal / Object.isSealed: 密封一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
拓展方法
Ojbect.preventExtensions(o) // 禁止拓展
Class
实现私有变量可以靠 WeakMap
(无引用即被回收) 或 Symbol
Reflect & Proxy
Reflect
- Object 语言内部方法: 将 Object 对象的一些属于语言内部的方法如
Object.defineProperty
, 放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。 - Object 部分方法的返回值改变: 让其变得更合理。比如,
Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回false
. - object 操作函数化: 如
name in obj
->Reflect.has(obj, name)
,delete obj[name]
->Reflect.deleteProperty(obj, name)
- 与
Proxy
方法一一对应: 让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。
Proxy
- 修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种 “元编程”(meta programming),即对编程语言进行编程。
- 在目标对象之前架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写.
基本用法 basic usage
p = new Proxy(target={}, handler={
// 拦截: 对象属性的读取
// !! 注意 target 对象属性设置, 如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
get(target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
// 拦截: 该对象属性的设置
// receiver 一般情况下是 p (Proxy 实例) 本身
set(target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
},
// 拦截: propKey in obj, 但不拦截 for...in, 不对迭代生效
has(target, propKey) {},
// 拦截: delete obj[propKey], Reflect.deleteProperty(target, prop)
deleteProperty(target, propKey){
// 例: 不允许删除
// throw new TypeError()
},
// 拦截: p 作为函数调用的操作
// proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
apply(target, object, args){},
// 拦截: new, p 作为构造函数调用的操作
// 必须返回对象 (引用类型), 否则 TypeError
construct(target, args, newTarget) {
return new target(...args) // 默认行为
},
// 拦截 Object.defineProperty, Object.defineProperties
// 优先度低于 writable, configurable
// 返回 boolean, false 意味添加属性无法生效
defineProperty(target, propKey, propDesc) {},
//拦截 Object.keys(proxy), for...in, Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy) 循环,返回一个数组。
// 该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的 可遍历 属性。
ownKeys(target){},
// ------------------- 非常用 --------------------
// 拦截 Object/Reflect.getPrototypeOf(p), Object.prototype.isPrototypeOf(), Object.prototype.__proto__, instanceof
// 返回一个对象。
getPrototypeOf(target) {},
// 拦截 Object.setPrototypeOf(proxy, proto),
// 返回一个布尔值。
// 如果目标对象是函数,那么还有两种额外操作可以拦截。
setPrototypeOf(target, proto) {},
// 拦截 Object.preventExtensions(proxy)
// 返回一个布尔值。
preventExtensions(target) {},
// 拦截 Object.isExtensible(proxy)
// 返回一个布尔值
isExtensible(target){},
// 拦截 Object.getOwnPropertyDescriptor() (返回 defineProperty 的配置)
// 返回 属性描述对象 或者 undefined
getOwnPropertyDescriptor(target, key){
if (key[0]==='_') return;
return Object.getOwnPropertyDescriptor(target, key);
},
});
用法举例
安全枚举类型, 用作 state machine action type
export default function enum(object) {
return new Proxy(object, {
get(target, prop) { // 读取不存在属性时报错
if (target[prop]) return Reflect.get(target, prop)
throw new ReferenceError(`Unknown enum '${prop}'`)
},
set() { // 不能动态改变
throw new TypeError('Enum is readonly')
},
deleteProperty() { // 不能删除
throw new TypeError('Enum is readonly')
}
})
}
测试, 断言, 如监听函数调用情况
function spy(spyFn) {
spyFn.toBeCalledTimes = 0
spyFn.lastCalledWith = undefined
return new Proxy(spyFn, {
apply(target, thisArg, argumentsList) {
target.toBeCalledTimes += 1
target.lastCalledWith = argumentsList.join(', ')
}
})
}
// 使用
spyApplyColor = spy(applyColor)
colors.forEach(color => spyApplyColor(color))
// 测试
expect(callback.toBeCalledTimes).toBe(colors.length)
expect(callback.lastCalledWith).toBe(colors[1])
数组
let arr = [1,2,3,4]
const keys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
let p=new Proxy(arr, {
get(target, prop, receiver) {
// 浏览器下无问题, 不需要判断
// Node.js 下 过滤
// Symbol(nodejs.util.inspect.custom)
// Symbol(Symbol.toStringTag)
// Symbol(Symbol.iterator)
if (typeof prop!=='symbol') {
keys.indexOf(prop) > -1 && console.log(propKey);
let index = Number(prop);
if (index<0) prop = String(target.length + index);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value) {
if (typeof prop!=='symbol' && !isNaN(Number(prop)))
console.log(`set arr[${prop}]=${value}`)
target[prop] = value
return true
}
})
p.push(5) // push; set arr[4]=5;
console.log(p, p[-1]) // [ 1, 2, 3, 4, 5 ] 5
表单校验
let person = {
name: 'xiaoming',
age: 30
}
let handler = {
set (target, key, value, receiver) {
if (key==='name' && typeof value!=='string') {
throw new TypeError('姓名是字符串')
}
if (key==='age') {
if (typeof value!=='number'||value!==value<<0) throw new Error('年龄是正整数')
if (value<0||value>150) throw new RangeError('年龄范围0-150')
}
return Reflect.set(target, key, value, receiver)
}
}
let boy = new Proxy(person, handler)
boy.name = 'xiaohong' // OK
boy.age = '18' // 报错 用户年龄必须是数字类型
对嵌套属性的支持
let obj = {
info: {
name: 'eason',
blogs: ['webpack', 'babel', 'cache']
}
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
// 递归创建并返回
if (typeof target[key]==='object' && target[key]!==null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
// 以下两句都能够进入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')
没有 proxy 时 Object.defineProperty / Object.defineProperties
// with its default value
Object.defineProperty(obj='target', prop='keyOrSymbol', descriptor={
// whether is picked by Object.assign(), ...;
// for non-Symbols, whether it is picked in for..in and Object.keys()
enumerable: false,
// can be deleted or its attributes can be changed, throw TypeError
// cannot be reassigned
// if true, can reassign enumerable
configurable: false,
// if false, the prop value cannot be changed, in strict mode throw TypeError 'prop is read-only'
// cannot be reassigned
writable: false,
value: undefined,
get() {},
set() {},
})
区别
definedProperty
的作用是劫持一个对象的属性,劫持属性的getter
和setter
方法,在对象的属性发生变化时进行特定的操作。而 Proxy 劫持的是整个对象。- Proxy 会返回一个代理对象,我们只需要操作新对象即可,而 Object.defineProperty 只能遍历对象属性直接修改。
definedProperty
不支持数组,更准确的说是不支持数组的各种 API。而 Proxy 可以支持数组的各种 API。- 兼容性, Proxy 无法 polyfill
Vue3.0 应用
Symbol
// create
a = Symbol('new') // new Symbol
Symbol({ a:1 }); // Symbol([object Object]), .toString()
new Symbol('new') // Uncaught TypeError: Symbol is not a constructor
Symbol.for('1')===Symbol.for('1') // create same symbol
// enum
obj = {
key:'value',
[Symbol('name')]:'symbol'
}
Object.getOwnPropertyNames(obj); // ["key"]
Object.keys(obj); // ["key"]
for (var i in obj) { console.log(i) } // key
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
Reflect.ownKeys(obj) // ['key', Symbol(name)]
重要新增 symbol 属性
Symbol.iterator
// 是否能遍历 + 使用 for...of (可遍历意味可展开)
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
[...myIterable] // [1, 2, 3]
for (let v of myIterable) { console.log(v) } // 1, 2, 3
Symbol.toPrimitive
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number': // 需要转换为数值
return 123;
case 'string': // 需要转换为 string
return 'str';
case 'default': // 可以转成数值, 也可以转成 string
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
Symbol.toStringTag
影响 Object.prototype.toString.call(o)
的结果
({[Symbol.toStringTag]: 'Foo'}.toString()) // 'Foo'
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
Symbol.hasInstance
定义 instanceof 的返回结果
应用
- 因 JSON中不能保存
Symbol
, 可用以防止xss
// JSON let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* put your exploit here */' }, }, }; let message = { text: expectedTextButGotJSON }; <p>{message.text}</p>
- React 判断有效
ReactElement
var REACT_ELEMENT_TYPE = (typeof Symbol==='function' && Symbol.for && Symbol.for('react.element')) || 0xeac7; ReactElement.isValidElement = function (object) { return typeof object==='object' && object!==null && object.$$typeof===REACT_ELEMENT_TYPE; };
- 因其不可枚举, 可用作私有属性, 如
[Symbol.toPrimitive]
,[Symbol.Iterator]
const privateField = Symbol(); class myClass { constructor(){ this[privateField] = ''; } getField(){ return this[privateField]; } setField(val){ this[privateField] = val; } }
- 防属性/键值碰撞
Function
name
var f = function () {};
var ff = f
// ES5
f.name // ""
// ES6
f.name // "f"
ff.name // 'f'
(new Function).name // "anonymous"
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
参数默认值, ...rest
- 对
length
(函数预期传入的参数个数) 的影响: 指定默认值的参数及之后的参数都不计入,...
参数也不会计入。(function (a, b = 5, c) {}).length // 1 (function (a, ...args) {}).length // 1
- 对
作用域
, 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。var x = 1; (function f(x, y = x) { console.log(y); })(2) // 2 (function f(y = x) { let x = 2; console.log(y); })() // 1 (function foo(x = x) {})() // ReferenceError: x is not defined // 上面代码 "x = x" 形成一个单独作用域。实际执行的是 let x = x,由于暂时性死区,这行代码会报错 ”x 未定义 “。 (function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); })() // 3 x // 1
JS Collection 集合
初学者应该了解的数据结构:Array、HashMap 与 List
Array
.from(arrayLike) // convert arrayLike to array
Q & A
// Reflect 会返回 symbol 属性 key, getOwnPropertyNames 会返回不可枚举的属性 key Reflect.ownKeys(obj).length ≥ Object.getOwnPropertyNames(obj).length ≥ Object.keys(obj).length
Proxy
可以实现很多以前只有魔改 JS 引擎底层才能实现的效果,请找出下面是利用 Proxy 实现了的神奇效果:
A. 原型就是自己的对象 —— Object.getPrototypeOf(obj)===obj // true
B. 任意属性都存在的对象 —— "任意名字的属性" in obj // true
C. 任意值都是它的实例的对象,甚至 null 和 undefined —— undefined instanceof obj // true
D. 用 Object.prototype.toString() 检测出来的对象类型是 haha 的对象 —— Object.prototype.toString.call(obj)==="[object haha]" // true
E. 一元加后的值与加 0 后的值分别恒等于两个不同的数字 —— 比如 +obj 始终===1,但 obj + 0 始终等于===10
F. 亦假又亦真的对象 —— if (obj) {alert("执行不到")} 但 if (obj.length) {alert("能执行到")}
参考答案:AB
A:
obj = new Proxy({}, {getPrototypeOf(){return obj}})
B:
obj = new Proxy({}, {has(){return true}})
C:
obj = {[Symbol.hasInstance](){return true}}
D:
obj = {[Symbol.toStringTag]: "haha"}
E:
obj = {[Symbol.toPrimitive](hint){return hint==="number" ? 1 : 10}}
F: document.all
具有其性质
Promise
实现要点
- status = 'pending' | 'resolved' | 'rejected';
- value = undefined, reason = undefined, 分别在 resolve / reject 后赋值给参数
- onFullfilledArray = [], onRejectedArray = [], 分别在 resovle / reject 时依次执行
- resolve 后 return 一个 myPromise
- 入参检查 (resolve, reject)
- then 绑在 prototype 上, 用递归把 全部 then 的回调绑在 onFullfilledArray, onRejectedArray 上
- 定义 resolvePromise
注意事项
- 在 Pending 转为另外两种之一的状态时候,状态不可在改变..
- Promise 的
then
为异步。而new Promise()
构造函数内为同步 - Promise 的
catch
不能捕获任意情况的错误 (比如 then 里面的 setTimout 内手动抛出一个 Error) - Promise 的 then 返回 Promise.reject() 会中断链式调用
- Promise 的 resolve 若是传入值而非函数,会发生值穿透的现象
- Promise 的
catch, then
,return 的都是一个新的Promise
(在 Promise 没有被中断的情况下)
Promise.all
Promise.all
(iterable) 在下列情况任一满足时返回
- iterable 为空时, 同步 立即返回 Promise.all([])
- 所有在可迭代参数中的 promises 完成 (返回
[promise]
) - 任一
promise
reject
(返回rejected promise
), 如果是立即 reject, 则同步 reject
例如: 有四个 promise 在一定的时间之后调用成功函数,有一个立即调用失败函数,那么 Promise.all 将立即变为失败。
链式调用
let task = Promise.resolve()
for (let i = 0; i < promises.length; i++) {
task = task.then(() => promises[i]).then(callback)
}
// or reduce
promises.reduce((task, p) => {
return task.then(() => p).then(callback)
}, Promise.resolve())
.race([PromiseLike])
将多个 Promise 实例,包装成一个新的 Promise 实例. 返回率先改变状态的 Promise
用来 request timeout
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
.allSettled([PromiseLike])
(ES2020)
当所有 Promise resolve / reject 时返回. 弥补 Promise.all 任一错误即抛弃其他结果的行为
.any()
(stage3)
- 任一 fulfill, 返回 fulfil
- 所有都 reject, 返回
AggregateError
: [rejectedPromise]
Generator
Generator 函数返回特殊遍历器对象, 只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行 + 懒运行的函数. yield
表达式就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下。
- 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在
yield
后面的那个表达式的值 (懒求值, 只有此时才会求值),作为返回的对象的value
属性值。 - 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
- 如果没有再遇到
yield
,就一直运行到函数到return
为止,并将 return 语句后面的表达式的值,作为返回的对象的value
属性值. 同时返回done: true
. - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined。
如果 Generator 无 yield
, 即为一个懒函数, 只有调用 .next()
时内部才执行
常见写法
function* genFn() {
yield 1
return 2
}
let obj = {
*getFn() {},
getFn2: function* () {} // 不能用箭头函数
}
// !! 注意要实例化后再用
var realFn = getFn()
realFn.next() // 1
yield
yield*
在一个 Generator 函数里面执行另一个 Generator / Iterator
函数 (返回全部遍历值, 而不是 [Generator] {}
)
如果是非 Generator 的 Iterator, 也可被遍历
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
// 依次遍历foo(), 等于
// yield 'a'; yield 'b';
// 或等于
// for (let v of foo()) {
// yield v;
// }
yield 'y';
}
for (let v of bar()){
console.log(v); // xaby
}
// 非 Generator
let read = (function* () {
yield* 'hello';
})();
read.next().value // "h"
上例, 只有 foo
有 return
时才能将值传递给 bar
function* foo() {
yield 'a';
return 'b'; // yield 'b' 时 log undefined
}
function* bar() {
var v = yield* foo()
console.log('log '+v)
return v
}
var it=bar()
console.log(it.next()) // { value: 'a', done: false }
console.log(it.next())
// !! 注意: log b 先一步打印
// log b
// { value: 'b', done: true }
console.log(it.next()) // { value: undefined, done: true }
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]
yield 用法
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK, undefined
console.log('Hello' + (yield 123)); // OK, 123
}
for...in vs for of
对象用 for...in
通常用 for...in
来遍历对象的键名
- 可以遍历到对象的原型方法及继承自原型链上的属性/方法, 如果不想遍历原型方法和属性的话,可以在循环内部判断一下,
hasOwnPropery
方法可以判断某属性是否是该对象的实例属性 - 如用来遍历数组, 则会返回非期望结果, 如以下会多返回
method
,name
Array.prototype.method=function(){ console.log(this.length); } var myArray=[1,2,4,5,6,7] myArray.name="数组" for (var index in myArray) { console.log(myArray[index]); // 返回 name, method }
迭代器用 for...of
for of
适用遍历数 / 数组对象 / 字符串 /map/set 等拥有迭代器对象的集合。但是不能遍历对象,因为没有迭代器对象。与 forEach()
不同的是,它可以正确响应 break、continue 和 return 语句
所有拥有 Symbol.iterator
的对象被称为可迭代的。可迭代对象的概念几乎贯穿于整门语言之中,不仅是 for of
循环,还有 Map 和 Set 构造函数、解构赋值,以及新的展开操作符。
for...of
循环首先调用集合的 Symbol.iterator 方法
,紧接着返回一个 新的迭代器对象
。迭代器对象可以是任意具有.next () 方法的对象; for...of
循环将重复调用这个方法,每次循环调用一次。
var zeroesForeverIterator = {
[Symbol.iterator]: function () {
return this;
},
next: function () {
return { done: false, value: 0 };
}
};