# 2021年核心面试题详解
# 1、有做过前端加载优化相关的工作吗? 都做过哪些努力?
# 1、有性能优化的目的是什么?
REF:页面性能检测: https://developers.google.com/speed/pagespeed/insights/
TIP:优化哪个指标完善了业务体验,解决了业务问题
首屏时间(白屏时间)
首次可交互时间
首次有意义内容渲染时间
# 2、常见优化手段
# 1、只请求当前需要的资源
- 异步加载
- 懒加载
- polyfill(适配高版本语法)的优化
解析:不用使用webpack打包,使用cdn链接 https://polyfill.io/v3/url-builder/,进行浏览器检测,实现浏览器的polyfill按需加载
# 2、缩减资源体积
打包压缩:webpack4已支持
gzip:减小静态资源的压缩算法,服务器默认开启
图片格式优化
对图片进行压缩:www.tinypng.com图片压缩网站
根据屏幕分辨率展示不同分辨率的图片
webp图片使用
尽量控制cookie大小
浏览器中request header中携带cookie,同域名请求会携带当前域名下的所有cookie
# 3、时序优化
js中promise.all:并发控制
ssr:服务器端渲染,可使用缓存,方便seo
prefetch、prerender、preload
资料:一站式理解 - prefetch preconnect prerender preload (opens new window)
<!-- 加载代码瞬间,预解析代码 -->
<link rel=“dns-prefetch” href=“xxxxxx” />
<!-- 加载代码瞬间,预链接 -->
<link rel=“preconnect” href=“xxxxxxx” />
<!-- 需申明类型,预加载 -->
<link rel=“preload” as=“image” href=“xxxxxxxxx” />
2
3
4
5
6
# 4、合理利用缓存
cdn
cdn预热(不通过用户访问,提前分发加载图片)、cdn刷新(强制回源)、一般cdn域名与实际域名不同,防止携带同源cookie
http缓存
localStorage, sessionStorage
# 3、如果一端js执行时间长,怎么去分析
装饰器ref:
用装饰器实现资源运行时间的计算
export function meatrue(target, name, descriptor){
const oldValue = descriptor.value
descriptor.value = async function(){
console.time(name)
const ret = await oldValue.apply(this, arguments)
console.timeEnd(name)
return ret
}
return descriptor
}
2
3
4
5
6
7
8
9
10
# 4、场景分析1:webp转换
阿里云oss支持通过链接后面拼参数来做图片的格式转换,尝试写一下,把任意图片转换为webp格式,要注意什么问题? 解析:考虑浏览器适配和边界,是否全在oss(Object Storage Service,对象存储服务)上。
解析:oss与cdn
oss的核心是存储,以及计算能力(图片处理)
cdn的核心是分发,本身不会给用户提供直接操作存储的入口
//检测浏览器是否支持webp格式
function checkWebp(){
try{
return (
document
.creatElement('canvas')
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0
)
}catch(err){
return false
}
}
const supportWebp = checkWebp();
//另需咨询是否所有图片在oss上
export function getWebpImageUrl(url) {
//判断url是否为空
if(!url){
return url
}
//判断url是否是base64字符串
if (url.startsWith('data:')) {
return url;
}
if (!supportWebp) {
return url;
}
return url + '?x-oss-process=image/format,webp'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 5、场景分析2:并发加载图片
如果页面上有巨量图片需要展示,除了懒加载的方式,有没有其他方法限制一下同时加载的图片数量?
代码题,通过实现promise的并发控制。
数组常用方法
push() //向数组的末尾添加一个或更多元素,并返回新的长度
pop() //删除数组的最后一个元素并返回删除的元素。
shift() //删除并返回数组的第一个元素。
unshift() //向数组的开头添加一个或更多元素,并返回新的长度。
splice() //添加或删除原素 array.splice(index,howmany,item1,.....,itemX),然后返回被删除的项目
//可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。
sort() //array.sort(sortfunction)
reverse() //颠倒顺序后的数组
filter()//过滤返回新数组
concat //array1.concat(array2,array3,...,arrayX),连接数组,返回连接副本
slice() //选取数组的一部分,并返回一个新数组。array.slice(start, end)
arr.includes(searchElement)//数组是否有该元素,include
arr.includes(searchElement, fromIndex)
array.map(function(currentValue,index, arr), thisValue)
//循环操作数据返回新值,map
/*
currentValue 必须。当前元素的值
index 可选。当前元素的索引值
arr 可选。当前元素属于的数组对象
thisValue 可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。
如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
*/
arr.reduce(callback,[initialValue]) //求和、数据扁平化、数据去重
/* 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素
callback (执行数组中每个值的函数,包含四个参数)
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)可选
4、array (调用 reduce 的数组)可选
initialValue (作为第一次调用 callback 的第一个参数。)
*/
Array.from(arrayLike, mapFn, thisArg)
/* 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
1、arrayLike:想要转换成数组的伪数组对象或可迭代对象。
2、mapFn(可选):如果指定了该参数,新数组中的每个元素会执行该回调函数。
3、thisArg(可选):可选参数,执行回调函数 mapFn 时 this 对象。
Array.from(arrayLike) <=> [].slice.call(arrayLike)
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function limitLoad(urls, handler, limit){
const sequence = [].concat(urls) //copy array
let promises = [] //promise池
//splice取出前limit元素,并改变原数组
promises = sequence.splice(0, limit).map((url, index)=>{
//这里返回的 index 是任务在 promises 的脚标,
//用于在 Promise.race 之后找到完成的任务脚标
return handler(url).then(() => {
return index
});
})
let p = Promise.race(promises) //获取最快完成的任务脚标
for(let i=0; i<sequence.length; i++){//sequence已去除前3元素
//链式方法完成顺序推入
/* 解析:p=Promise.race(0,1,2).then(return Promise.race(1,2,3)).then(return Promise.race(2,3,4)) for循环每次链式加入一个元素*/
p = p.then((res) => {
promises[res] = handler(sequence[i]).then(() => {
return res
})
return Promise.race(promises)
})
}
}
const urls = [{
info: 'link1',
time: 3000
},
{
info: 'link2',
time: 2000
},
{
info: 'link3',
time: 5000
},
{
info: 'link4',
time: 1000
},
{
info: 'link5',
time: 1200
},
{
info: 'link6',
time: 2000
},
{
info: 'link7',
time: 800
},
{
info: 'link8',
time: 3000
},
];
// 设置我们要执行的任务
function loadImg(url) {
return new Promise((resolve, reject) => {
console.log("----" + url.info + " start!")
setTimeout(() => {
console.log(url.info + " OK!!!")
resolve()
}, url.time)
})
}
limitLoad(urls, loadImg, 3)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 2、平时有关注过前端的内存处理吗?
# 1、内存的生命周期
内存分配:当我们申明变量、函数、对象的时候,js会自动为他们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
# 2、js中的垃圾回收机制
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。
引用计数垃圾回收
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需了。存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
标记清除算法
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
- 从根部出发将能触及到的对象的标记清除
- 那些还存在标记的变量被视为准备删除的变量
- 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间
# 3、JS中有哪些常见的内存泄漏
- 全局变量
function foo() {
bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();
2
3
4
5
- 未被清理的定时器和回调函数
如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); // 每 5 秒调用一次
2
3
4
5
6
7
- 闭包
在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 对于 'originalThing'的引用
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。 同时 unused 是一个引用了 originalThing 的闭包。 这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。 当这段代码被反复执行时,内存会持续增长。
- DOM引用
很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。
var elements = {
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
document.body.removeChild(document.getElementById('image'));
// 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}
2
3
4
5
6
7
8
9
10
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
# 4、如何避免内存泄漏
减少不必要的全局变量,使⽤严格模式避免意外创建全局变量。
在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
# 5、实现sizeOf函数,计算Object占用多少字节byte
# 1、js数据类型(8种)
- 基本类型(5+2)
- Number:64bit-8byte
- Boolean:32bit-4byte
- String:16bit-2byte
- null
- undefined
- symbol(ES6)
- BigInt(ES2020)
- 引用类型:object(Array,Function,Date)
# 2、数据类型判断
参考资料:彻底弄懂js数据类型判断 (opens new window)
- typeof:可判断number、string、boolean、Symbol、undefined及function,而对于null及数组、对象,typeof均检测出为object,不能进一步判断它们的类型。
let obj = {
name: 'zhangxiang'
};
function foo() {
console.log('this is a function');
}
let arr = [1,2,3];
let s = Symbol();
console.log(typeof 1); // number
console.log(typeof '1'); //string
console.log(typeof true); //boolean
console.log(typeof s); //Symbol
console.log(typeof undefined); //undefined
console.log(typeof null); //object
console.log(typeof foo); //function
console.log(typeof obj); //object
console.log(typeof arr); //object
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- instanceof:其实适合用于判断自定义的类实例对象, 而不是用来判断原生的数据类型
- Object.prototype.toString:在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串
function foo(){};
Object.prototype.toString.call(1); '[object Number]'
Object.prototype.toString.call(NaN); '[object Number]'
Object.prototype.toString.call('1'); '[object String]'
Object.prototype.toString.call(true); '[object Boolean]'
Object.prototype.toString.call(undefined); '[object Undefined]'
Object.prototype.toString.call(null); '[object Null]'
Object.prototype.toString.call(Symbol());'[object Symbol]'
Object.prototype.toString.call(foo); '[object Function]'
Object.prototype.toString.call([1,2,3]); '[object Array]'
Object.prototype.toString.call({});'[object Object]'
2
3
4
5
6
7
8
9
10
11
- constructor:注意undefined和null没有contructor属性
console.log(num.constructor === Number);// true
其他
数组判断:Array.isArray()
NaN判断:Number.isNaN或者typeof n === "number" && window.isNaN( n )
DOM元素判断:!!(obj && obj.nodeType === 1)
对象判断及argument对象判断
isObject: function(obj){
var type = typeof obj;
return type === 'function' || typeof === 'object' && obj !== null;
}
isArguments: function(obj){
return Object.prototype.toString.call(obj) === '[object Arguments]' || (obj != null && Object.hasOwnProperty.call(obj, 'callee'));
}
2
3
4
5
6
7
# 3、sizeof函数实现
const testData = {
a: 111,
b: 'cccc',
2222: false,
}
function caculator(object){
const objectType = typeof object
switch(objectType){
case 'boolean':
return 4;
case 'string':
return object.length * 2;
case 'number':
return 8;
case 'object':
if(Array.isArray(object)){
return object.map(caculator).reduce(function(pre,cur){
return pre + cur
})
}else{
return caculateObj(object);
}
default:
return 0
}
}
const seen = new WeakSet()
function caculateObj(object){
if(object === null){
return 0
}
let bytes = 0
let keys = Object.keys(object)
for(let i=0; i<keys.length; i++){
let key = keys[i]
bytes += caculator(key)
if(typeof object[key] === 'object' && object[key] !== null){
if(seen.has(object[key])){
continue
}
seen.add(object[key])
}
bytes += caculator(object[key])
}
return bytes
}
console.log(caculator(testData))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
解析:
1、Set
- 成员唯一、无序且不重复
- 只有键值,没有键名
- 可以遍历,方法有add、delete、has
2、WeakSet
- 成员都是对象
- 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存Dom节点,不容易造成内存泄漏
- 不能遍历,方法有add、delete、has
3、Map
- 键值对的集合
- 可以遍历,方法很多可以转换为其他数据格式
4、WeakMap
- 只接受对象作为键名(非null),不接受其他类型的值做键名
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键值无效
- 不能遍历,方法有get、set、has、delete
# 3、来聊一下前端HTTP请求相关
# 1、平时怎么解决跨域问题
资料:前端常见跨域解决方案 (opens new window)
同源限制:
- cookie、localStorage、IndexDB无法读取
- DOM和js对象无法获取
- ajax请求无法发送
1、浏览器同源策略(Same Origin Policy,SOP):域名、协议、端口相同
2、跨域问题解决
- jsonp: JSON With Padding(填充式 JSON 或参数式 JSON)
原理:就是动态创建<script>
标签,然后利用<script>
的 src 属性不受同源策略约束来跨域获取数据
实现:JSONP 由两部分组成:回调函数 和 数据。回调函数是用来处理服务器端返回的数据,回调函数的名字一般是在请求中指定的。而数据就是我们需要获取的数据,也就是服务器端的数据。
参考:jsonp的原理及实现 (opens new window)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP实现跨域</title>
<script type="text/javascript">
function handleResponse(response){ //处理服务器返回的数据
console.log(response); //控制台输出
}
function foo() {
var script = document.createElement("script");
script.src = "http://192.168.31.122/1.txt";
//设置请求的链接以及处理返回数据的回调函数
document.body.insertBefore(script, document.body.firstChild);
//服务器直接返回handleResponse(x, x),解析后直接调用
}
</script>
</head>
<body>
<button id="btn" onclick="foo()">确定</button>
</body>
</html>
<!-- http://192.168.31.122/1.txt
handleResponse([ { "name":"xie",
"sex" :"man",
"id" : "66" },
{ "name":"xiao",
"sex" :"woman",
"id" : "88" },
{ "name":"hong",
"sex" :"woman",
"id" : "77" }]
-->
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
cors:跨域资源共享(Cross-origin resource sharing)提供资源服务器添加Access-Control-Allow-Origin
node正向代理:利用服务端不跨域的特性
/api -> 同域node服务 -> /api -> 前端
nginx反向代理:使用proxy_pass属性配置
/api -> 通过代理转接至/same/api
img标签
document.domain设置:主域名相同,子域名不通,设置document.domain为主域名解决,eg:未设置domian,通过iframe访问跨域域名进行cookie设置失败,设置后设置成功,再通过cookie进行会话保持访问跨域资源页面。
Window.postMessage事件实现跨域数据获取和传输,设置targetOrigin保证数据传输安全。
- otherWindow.postMessage(message, targetOrigin, [transfer]);
- window.addEventListener("message", receiveMessage, false),message有三个属性:
- data:从其他 window 中传递过来的对象;
- origin:消息发送方的origin
- source:对发送消息的窗口对象的引用,用来在具有不同origin的两个窗口之间建立双向通信
# 2、有做过全局的请求处理吗?统一处理登录态?统一处理全局错误?
- Axios库
- adaptar适配器
- interceptor拦截器:request、response
# 3、你能给xhr添加hook,实现在各个阶段打日志吗?
代码题, 实现页面上通过xhr发请求的时候, 在xhr的生命周期里, 能够实现自定义的行为触发。
解析:使用new XMLHttpRequest()进行请求发送时,例如open、onreadystatechange、onload、onerror时打印日志
/* 重写xhr的属性和方法
1.class的使用,new对象
2.this的指向
3.apply,call的使用
4.Object.definePorperty使用
5.代码的设计能力
6.hook的理解
*/
class XhrHoor{
/* 构造函数 */
constructor(beforehook={}, afterhook={}){
// 单例
if (XhrHook.instance) {
return XhrHook.instance;
}
this.Xhr = window.XMLHttpRequest
this.beforehook = beforehook
this.afterhook = afterhook
this.init = init()
XhrHook.instance = this
}
/* 初始化重写xhr对象 */
init(){
let _this = this
window.XMLHttpRequest = function () {
this._xhr = new _this.Xhr()
_this.overwrite(this) //通过this获取_xhr属性
}
}
/* 处理重写 */
overwrite(proxyXhr){
for(let key in proxyXhr._xhr){//对象遍历for in
if(typeof proxyXhr._xhr[key] === 'function'){
overwriteMethod(key, proxyXhr)
continue
}
overwriteAttributes(key, proxyXhr)
}
}
/* 重写方法 */
overwriteMethod(key, proxyXhr){
let beforehook = this.beforehook //可以拦截原有行为
let afterhook = this.afterhook
proxyXhr[key] = (...args)=>{ //
if(beforehook[key]){
const res = beforehook[key].call(proxyXhr, ...args)
if(res === false){ //拦截行为
return
}
}
const res = proxyXhr._xhr[key].apply(proxyXhr._xhr, args)
afterhook[key] && afterhook[key].call(proxyXhr._xhr, res) //将res执行结果传入
return res
}
}
/* 重写属性 */
overwriteAttributes(key, proxyXhr){
Object.defineProperty(proxyXhr, key, this.setPropertyDescriptor(key, proxyXhr))
}
/* 设置属性的属性描述 */
setPropertyDescriptor(key, proxyXhr){
let obj = Object.create(null) //创建空对象
let _this = this
obj.set = function(val){
if(!key.startsWith('on')){//只重写on开头的属性
proxyXhr['__' + key] = val
return
}
if(_this.beforehook[key]){
this._xhr[key] = function(...args){
_this.beforehook.call(...args)
val.apply(proxyXhr, args)
}
}
this._xhr[key] = val
}
obj.get = function(){
return proxyXhr['__' + key] || this._xhr[key] //优先返回自定义属性
}
return obj
}
}
/* 使用hook,建立XhrHoor时重写xhr */
new XhrHoor({
open: function () {
console.log('open');
return false //返回false不执行原有函数,返回true继续执行原有函数
},
onload: function () {
console.log('onload');
},
onreadystatechange: function () {
console.log('onreadystatechange');
},
onerror: function () {
console.log('hook error')
}
})
/* 使用被重写的xhr,各阶段会使用hook进行打印 */
var xhr = new XMLHttpRequest();
//方法
xhr.open('GET', 'https://www.baidu.com', true);
xhr.send();
//属性
xhr.onreadystatechange = function (res) {
console.log('statechange');
}
xhr.onerror = function () {
console.log('error');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
解析:
1、rest参数(...变量名),rest参数搭配变量是一个数组
function sortNumbers(){
return Array.prototype.slice.call(arguments).sort();
}
const sortNumbers = (...numbers) => numbers.sort();
2
3
4
5
2、Object.defineProperty(obj, prop, desc)
ref资料:深入浅出Object.defineProperty() (opens new window)
3、数组和对象遍历
ref资料:js中各种遍历方法 (opens new window)
# 4、平时用过发布订阅模式吗?比如Vue的event bus,node的eventemitter3
class EventEmitter {
constructor(maxListeners) {
this.events = {} //监听key-value
this.maxListeners = maxListeners || Infinity;
}
on(event, cb) {
if (!this.events[event]) {
this.events[event] = []
}
if (this.maxListeners !== Infinity && this.events[event].length >= this.maxListeners) {
console.warn(`${event} has reached max listeners.`)
return this;
}
this.events[event].push(cb)
return this
}
/* 无cb全部移除:事件名、callback */
off(event, cb) {
if (!cb) {
this.events[event] = null
} else {
this.events[event] = this.events[event].filter(item => item !== cb);
}
return this //链式调用
}
once(event, cb) {
const func = (...args) => {
this.off(event, func) //先移除
cb.apply(this, args)
}
this.on(event, func)
return this
}
emit(event, ...args) {
const cbs = this.events[event]
if (!cbs) {
console.warn(`${event} event is not registered.`);
return this;
}
cbs.forEach(cb => cb.apply(this, args))
return this
}
}
const add = (a, b) => console.log(a + b)
const log = (...args) => console.log(...args)
const event = new EventEmitter()
event.on('add', add)
event.on('log', log)
event.emit('add', 1, 2) // 3
event.emit('log', 'hi~') // 'hi~'
event.off('add')
event.emit('add', 1, 2) // Error: add event is not registered.
event.once('once', add)
event.emit('once', 1, 2) // 3
event.emit('once', 1, 2)
event.emit('once', 1, 2)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67