【直接收藏】前端JavaScript面试100问(下)

61、常见的 HTTP 请求有哪些 ? 他们的区别是什么 ?

常见的有5种,分别是GET、HEAD, POST、PUT、 DELETE

  • GET:它是最常见的方法,用于获取资源,常用于向服务器查询某些信息。打开网页一般都是用GET方法,因为要从 Web 服务器获取信息

  • HEAD:类似于 GET请求,只不过返回的响应中没有具体的内容,用于获取报头。

  • POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件), 数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或对已有资源的修改。

  • PUT:从客户端向服务器传送的数据取代指定文档的内容。

  • DELETE:请求服务器删除指定的页面。

最常见的HTTP请求方法是GET 和 POST。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。GET和POST的区别:

  • GET提交的数据会放在?之后,以问号(?)分割URL 和传输数据,参数之间以&相连

  • GET提交的数据大小有限制(因为浏览器对URL的长度有限制), 而POST 方法提交的数据大小没有限制。

  • GET方式提交数据会带来安全问题,比如一个登录页面通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。

62、 JS 的数据类型有哪些 ? 如何判断数据类型 ?他们的优缺点是什么?

  • typeof 用来检测数据类型的运算符

    检测的不管是数组还是正则都返回的是"object",所以typeof不能判断一个值是否为数组

  • instanceof/constructor。检测某一个实例是否属于某一个类使用instanceof/constructor可以检测数组和正则

    用instanceof检测的时候,只要当前的这个类在实例的原型链上(可以通过原型链__proto__找到它),检测出来的结果都是true。

    基本数据类型的值是不能用instanceof来检测的

    在类的原型继承中,instanceof检测出来的结果其实是不准确的

  • Object.prototype.toString.call(value) ->找到Object原型上的toString方法,让方法执行,并且让方法中的this变为value(value->就是我们要检测数据类型的值)。检测的类型比较多,也比较精准。

63、 symbol 你是怎么理解的 ?

Symbol 是 ES6 新推出的一种基本类型,它表示独一无二的值

它可以选择接受一个字符串作为参数或者不传,但是相同参数的两个Symbol值不相等

//不传参数
   const s1 = Symbol();
   const s2 = Symbol();
   console.log(s1 === s2); // false

   // 传入参数
   const s3 = Symbol('debug');
   const s4 = Symbol('debug');
   console.log(s3 === s4); // false

可以通过typeof判断是否为Symbol类型

console.log(typeof s1); // symbol

Symbol.for():用于将描述相同的Symbol变量指向同一个Symbol

let a1 = Symbol.for('a');
let a2 = Symbol.for('a');
a1 === a2  // true
typeof a1  // "symbol"
typeof a2  // "symbol"

let a3= Symbol("a");
a1 === a3      // false

Symbol.keyFor():用来检测该字符串参数作为名称的 Symbol值是否已被登记,返回一个已登记的 Symbol 类型值的key

let a1 = Symbol.for("a");
Symbol.keyFor(a1);    // "a"

let a2 = Symbol("a");
Symbol.keyFor(a2);    // undefined

description:用来返回Symbol数据的描述:

// Symbol()定义的数据
let a = Symbol("acc");
a.description  // "acc"
Symbol.keyFor(a);  // undefined

// Symbol.for()定义的数据
let a1 = Symbol.for("acc");
a1.description  // "acc"
Symbol.keyFor(a1);  // "acc"

// 未指定描述的数据
let a2 = Symbol();
a2.description  // undefined
  • 使用场景一:对象添加属性

let n = Symbol('N');
let obj = {
    name: "hello world",
    age: 11,
    [n]: 100
};
  • 使用场景二:给对象添加私有属性

const speak = Symbol();
class Person {
    [speak]() {
        console.log(123)
    }
}
let person = new Person()
console.log(person[speak]())

64、数组常用方法有那些

数组的常用方法 这样的面试题 算是非常基础的面试题 面试官的目的 也不会只是单纯的让你背诵出 数组的所有方法

这里的关键点 是 常用 这两个字 面试官的 目的是 通过 这个问题 看你平时在项目中 对于 数组函数的应用和理解 然后判断出 你平时在项目中对于数组的应用 然后推测出你真实的技术水平

这里建议的回答方式是 通过一个 自己用的最多的数组函数方法 深入展开的说一说 在 实际项目中的应用

例如谈到 数组单元删除 数组,splice() 除了要说 函数的用法之外 还要谈到 具体的项目中 删除数组单元之后 数组坍塌的影响 以及如何处理

concat() 连接两个或更多的数组,并返回结果。

join() 把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。

pop() 删除并返回数组的最后一个元素。 

shift() 删除并返回数组的第一个元素

push() 向数组的末尾添加一个或更多元素,并返回新的长度。

unshift() 向数组的开头添加一个或更多元素,并返回新的长度。

reverse() 颠倒数组中元素的顺序。

slice() 从某个已有的数组返回选定的元素

sort() 对数组的元素进行排序

splice() 删除元素,并向数组添加新元素。

toSource() 返回该对象的源代码。

toString() 把数组转换为字符串,并返回结果。

toLocaleString() 把数组转换为本地数组,并返回结果。

valueOf() 返回数组对象的原始值

65、JavaScript如何存储cookie

基本语法是 document.cookie = '键名=键值;expires=时间对象;path=路径' ;

时效 如果不设定 默认是 seeion 会话时效路径 如果不设定 默认是 当前文件所在文件夹

设定时效 要 设定一个时间对象 时间对象的时间戳 就是 时效期要注意计算 当前时区 和 世界标砖时间的时间差

路径一般设定为根目录 也就是 '/'

66、柯理化函数

所谓的柯里化函数 指的是 把接受多个参数的函数变换成接受一个单一参数的函数 并且返回接受余下的参数而且返回结果的新函数

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

优点:

1, 参数复用

例如 一个函数 有两个参数 但是第一个参数会被反复使用 每次都需要输入 一个重复的参数
使用柯里化函数之后 只需要 输入一个参数就可以了

2, 提前确认

提前定义好一个参数 也就 决定了整个函数程序的执行方向 避免每次都执行判断比较等

缺点:

只能提前定义一个参数 如果想要提前定义多个参数 这样的语法是不支持

柯里化函数执行效能上的问题:

存取arguments对象通常要比存取命名参数要慢一点
一些老版本的浏览器在arguments.length的实现上是相当慢的
使用 函数.apply() 和 函数.call() 通常比直接调用 fn() 稍微慢点
创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

67、对象遍历方法

JavaScript中 对象的遍历方法

for...in

基本语法是 for( 变量 in 对象 ){ 循环体程序 }

这里要注意的是
1, 变量中存储的键名 通过键名获取对象中存储的键值
  因为是变量 点语法取值 不支持解析变量 要使用 对象[键名] 获取键值

2, 循环变量 定义 let 和 var 定义 执行效果是不同的

Object.keys( 对象 )

返回一个数组 是 当前对象 所有键名组成的数组
之后再循环遍历这个数组 再执行操作

Object.value( 对象 )

返回一个数组 是 当前对象 所有键值组成的数组
之后再循环遍历这个数组 再执行操作

68、数组扁平化

数组扁平化

所谓的数组扁平化就是将多维数组转化为一维数组一般数组扁平化,数组中存储的多维数据都是数组 不会是对象或者函数

最常用的方法 就是 数组.toString() 将数组转化为字符串 结果是 获取数组中的每一个单元的数据 组成一个字符串 使用逗号间隔 再 以逗号为间隔 将字符串 转化为数组

function fun1( arr ){
   let str = arr.toString();
   return str.split(',');
}

还可以使用 数组.some() 方法 判断数组中是不是还存在数组 在使用 展开运算符 赋值

function fun1(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

另外 ES6 语法中 新增的 flat函数也可以实现数组的扁平化参数是固定的

const arr = 原始数组.flat( Infinity );

69、typeof 原理

利用 typeof 是根据返回值的结果来判断数据类型

具体返回值 一共是 number, string, object, boolean, function, undefined

其中 数组 null 对象 的返回值 都是 object这样的话具体的数据类型就不能区分的非常明确 在实际项目中 就不能准确的区分

如果想要具体的 区分 数据类型 需要使用 Object.prototype.toString.call() 方法 返回值是

object String 字符串
object Number 数值类型
object Boolean 布尔类型
object Undefined undefined类型
object Null null类型
object Function 函数类型
object Array 数组类型

70、介绍类型转化

JavaScript 因为是 弱类型计算机语言 存储数据时 对变量储存的数据类型没有设定因此一个变量中可以存储任意类型的数据 在程序的执行过程中 就会遇到需要数据类型转化的情况

自动转化
自动转化为字符串
数据 直接转化为 对应的字符串

        自动转化为数值类型    转化为 对应的数值       1      true      0      false  null ""  ''    

符合数字规范的字符串
转化为 NaN
        不符合数字规范的字符串
          undefined

自动转化为数值类型
          false:
              0 0.0000 ''   NaN   null     undefined
          true:
              其他情况都转化为 true

强制转化

强制转化为布尔类型

Boolean( 变量 / 表达式 )
              转化原则 和 自动转化原则完全相同
              false : 0   0.000   ''   null   NaN undefined
              true : 其他情况都转化为true

  强制转化为字符串类型
      String( 变量 / 表达式 );
          转化原则 和 自动转化原则完全相同
          不会改变 变量中存储的原始数据
    变量.toString( 进制 ) ;
          转化原则 和 自动转化原则完全相同
          不会改变 变量中存储的原始数据
          如果是 整数类型 可以 设定 转化的进制

          变量 存储 null 或者 undefined不支持

  强制转化为数值类型
      Number()
          转化原则 和 自动转化完全相同
          不会改变 变量中存储的原始内容

      parseInt()
          从 左侧起 获取符合整数语法规范的内容部分
          如果 左起第一个字符就不符合整数语法规范
          执行结果是 NaN

      parseFloat()
          从 左侧起 获取符合浮点数语法规范的内容部分
          如果 左起第一个字符就不符合浮点数语法规范
          执行结果是 NaN    

71、执行上下文

执行上下文:指当前执行环境中的变量、函数声明,参数(arguments),作用域链,this等信息。分为全局执行上下文、函数执行上下文,其区别在于全局执行上下文只有一个,函数执行上下文在每次调用函数时候会创建一个新的函数执行上下文。

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。

变量对象式一个抽象的概念,在不同的上下文中,表示不同的对象:

全局执行上下文的变量对象全局执行上下文中,变量对象就是全局对象。在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象。函数执行上下文的变量对象函数上下文中,变量对象VO就是活动对象AO。初始化时,带有arguments属性。函数代码分成两个阶段执行进入执行上下文时,此时变量对象包括形参函数声明,会替换已有变量对象变量声明,不会替换形参和函数函数执行

执行上下文栈的作用是用来跟踪代码的,由于JS是单线程的,每次只能做一件事情,其他的事情会放在指定的上下文栈中排队等待执行。

JS解释器在初始化代码的时候,首先会创建一个新的全局执行上下文到执行上下文栈顶中,然后随着每次函数的调用都会创建一个新的执行上下文放入到栈顶中,随着函数执行完毕后被执行上下文栈顶弹出,直到回到全局的执行上下文中。

首先创建了全局执行上下文,当前全局执行上下文处于活跃状态。全局代码中有2个函数 getName 和 getYear,然后调用 getName 函数,JS引擎停止执行全局执行上下文,创建了新的函数执行上下文,且把该函数上下文放入执行上下文栈顶。getName 函数里又调用了 getYear 函数,此时暂停了 getName 的执行上下文,创建了 getYear 函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。当 getYear 函数执行完后,其执行上下文从栈顶出栈,回到了 getName 执行上下文中继续执行。当 getName 执行完后,其执行上下文从栈顶出栈,回到了全局执行上下文中。

72、闭包的问题和优化

闭包:是指有权访问另外一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另外一个函数。

作用:

1、可以读取函数内部的变量2、相当于划出了一块私有作用域,避免数据污染;3、让变量始终保存在内存中

闭包有三个特性:

1.函数嵌套函数

2.函数内部可以引用外部的参数和变量

3.参数和变量不会被垃圾回收机制回收

闭包的问题

闭包会产生不销毁的上下文,会导致栈/堆内存消耗过大,有时候也会导致内存泄漏等,影响页面的运行性能,所以在真实项目中,要合理应用闭包!

闭包的优化

  原始代码

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

优化代码

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

73、浏览器和Node事件循环的区别

一、全局环境下this的指向

在node中this指向global而在浏览器中this指向window,这就是为什么underscore中一上来就定义了一 root;

而且在浏览器中的window下封装了不少的API 比如 alert 、document、location、history 等等还有很多。我门就不能在node环境中xxx();或window.xxx();了。因为这些API是浏览器级别的封装,存javascript中是没有的。当然node中也提供了不少node特有的API。

二、js引擎

在浏览器中不同的浏览器厂商提供了不同的浏览器内核,浏览器依赖这些内核解释折我们编写的js。但是考虑到不同内核的少量差异,我们需要对应兼容性好在有一些优秀的库帮助我们处理这个问题比如jquery、underscore等等。

  nodejs是基于Chromes JavaScript runtime,也就是说,实际上它是对GoogleV8引擎(应用于Google Chrome浏览器)进行了封装。V8引 擎执行Javascript的速度非常快,性能非常好。

NodeJS并不是提供简单的封装,然后提供API调用,如果是这样的话那么它就不会有现在这么火了。Node对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好。例如,在服务器环境中,处理二进制数据通常是必不可少的,但Javascript对此支持不足,因此,V8.Node增加了Buffer类,方便并且高效地 处理二进制数据。因此,Node不仅仅简单的使用了V8,还对其进行了优化,使其在各环境下更加给力。

三、DOM操作

浏览器中的js大多数情况下是在直接或间接(一些虚拟DOM的库和框架)的操作DOM。因为浏览器中的代码主要是在表现层工作。但是node是一门服务端技术。没有一个前台页面,所以我门不会再node中操作DOM。

四、I/O读写

与浏览器不同,我们需要像其他服务端技术一样读写文件,nodejs提供了比较方便的组件。而浏览器(确保兼容性的)想在页面中直接打开一个本地的图片就麻烦了好多(别和我说这还不简单,相对路径。。。。。。试试就知道了要么找个库要么二进制流,要么上传上去有了网络地址在显示。不然人家为什么要搞一个js库呢),而这一切node都用一个组件搞定了。

五、模块加载

 javascript有个特点,就是原生没提供包引用的API一次性把要加载的东西全执行一遍,这里就要看各位闭包的功力了。所用东西都在一起,没有分而治之,搞的特别没有逻辑性和复用性。如果页面简单或网站当然我们可以通过一些AMD、CMD的js库(比如requireJS 和 seaJS)搞定事实上很多大型网站都是这么干的。

  在nodeJS中提供了CMD的模块加载的API,如果你用过seaJS,那么应该上手很快。
node还提供了npm 这种包管理工具,能更有效方便的管理我们饮用的库

74、移动端点击延迟

原因 :

为了确定用户是要做单击 还是双击 还是要做其他的操作 因此移动端 当你点击时 会有 300毫秒延迟 为了等待判断用户的下一步操作是什么

解决方案1

禁用缩放

<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">


当HTML文档头部包含以上meta标签时 表明这个页面是不可缩放的,那双击缩放的功能就没有意义了,此时浏览器可以禁用默认的双击缩放行为并且去掉300ms的点击延迟。
这个方案有一个缺点,就是必须通过完全禁用缩放来达到去掉点击延迟的目的,然而完全禁用缩放并不是我们的初衷,我们只是想禁掉默认的双击缩放行为,这样就不用等待300ms来判断当前操作是否是双击。但是通常情况下,我们还是希望页面能通过双指缩放来进行缩放操作,比如放大一张图片,放大一段很小的文字。

解决方案2 更改默认的视口宽度

<meta name="viewport" content="width=device-width">

一开始,为了让桌面站点能在移动端浏览器正常显示,移动端浏览器默认的视口宽度
并不等于设备浏览器视窗宽度,而是要比设备浏览器视窗宽度大,通常是980px。
我们可以通过以下标签来设置视口宽度为设备宽度。因为双击缩放主要是用来改善
桌面站点在移动端浏览体验的,而随着响应式设计的普及,很多站点都已经对移动
端坐过适配和优化了,这个时候就不需要双击缩放了,如果能够识别出一个网站是
响应式的网站,那么移动端浏览器就可以自动禁掉默认的双击缩放行为并且去掉
300ms的点击延迟。如果设置了上述meta标签,那浏览器就可以认为该网站已经
对移动端做过了适配和优化,就无需双击缩放操作了。
这个方案相比方案一的好处在于,它没有完全禁用缩放,而只是禁用了浏览器默认
的双击缩放行为,但用户仍然可以通过双指缩放操作来缩放页面。

解决方案3 CSS touch-action

跟300ms点击延迟相关的,是touch-action这个CSS属性。这个属性指定了相应元素上能够触发的用户代理(也就是浏览器)的默认行为。如果将该属性值设置为touch-action: none,那么表示在该元素上的操作不会触发用户代理的任何默认行为,就无需进行300ms的延迟判断。


最后的最后 我们还可以使用一些 插件来解决这个问题 例如 FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。FastClick的实现原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后的click事件阻止掉。

安装    npm install fastclick -S

使用    如何你是vue项目可以在main.js里面直接引入,当然这样是全局的,如果你需要某个页面用到,那就单个页面引入。

//引入
import fastClick from 'fastclick'
//初始化FastClick实例。在页面的DOM文档加载完成后
fastClick.attach(document.body)

75、cookie属性

cookie的常见属性

键名 cookie键值对的键名
键值 cookie键值对的键值
expirescookie的时效 分为 session会话时效 时间时效 时间时效是服务器时间也就是世界标准时间
path路径 符合路径的文件才能访问cookie
httponly设置 为 true 了之后可以防止js程序访问 防止 xss攻击 增加cookie的安全性
secure设置 为 true 了之后cookie只能通过https协议发送 http协议是不能发送的 这样也是为了增加cookie的安全性

76、反柯里化

反柯里化的作用是,当我们调用某个方法,不用考虑这个对象在被设计时,是否拥有这个方法,只要这个方法适用于它,我们就可以对这个对象使用它

例如

Function.prototype.uncurring = function() {
  var self = this;
  return function() {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  };
};

我们先来看看上面这段代码有什么作用。

我们要把Array.prototype.push方法转换成一个通用的push函数,只需要这样做:

var push = Array.prototype.push.uncurring();

//测试一下
(function() {
  push(arguments, 4);
  console.log(arguments); //[1, 2, 3, 4]
})(1, 2, 3)

arguments本来是没有push方法的,通常,我们都需要用Array.prototype.push.call来实现push方法,但现在,直接调用push函数,既简洁又意图明了。

我们不用考虑对象是否拥有这个方法,只要它适用于这个方法,那就可以使用这个方法(类似于鸭子类型)。

我们来分析一下调用Array.prototype.push.uncurring()这句代码时,发生了什么事情:

Function.prototype.uncurring = function() {
  var self = this;  //self此时是Array.prototype.push

  return function() {
    var obj = Array.prototype.shift.call(arguments);
    //obj 是{
    //  "length": 1,
    //  "0": 1
    //}
    //arguments的第一个对象被截去(也就是调用push方法的对象),剩下[2]

    return self.apply(obj, arguments);
    //相当于Array.prototype.push.apply(obj, 2);

  };
};

//测试一下

var push = Array.prototype.push.uncurring();
var obj = {
  "length": 1,
  "0" : 1
};

push(obj, 2);
console.log( obj ); //{0: 1,1: 2, length: 2 }

看到这里你应该对柯里化和反柯里化有了一个初步的认识了,但要熟练的运用在开发中,还需要我们更深入的去了解它们内在的含义。

77、千分位

这里的需求 本质上是要 将 数字 转化为 带有千分位字符串 方法有很多

方法1 正则表达式

console.info( str.replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
  return s+','
}) )

方法2 字符串替换

console.info( str.replace(/(\d{1,3})(?=(\d{3})+$)/g,function($1){
  return $1=$1+','
}) )

方法3 数字转数组 反转后 添加 , 再反转回来拼接为字符串

console.info( str.split("").reverse().join("").replace(/(\d{3})+?/g,function(s){
  return s+",";
}).replace(/,$/,"").split("").reverse().join("") )

方法4 利用while循环拼接字符串每隔3个数字加一个分隔符,首尾不加

var result="",
  index = 0,
  len = str.length-1;
while(len>=0) {
  index%3===0&&index!==0 ? result+=","+str[len] : result+=str[len];
  len--;
  index++;
};
result=result.split("").reverse().join("");
console.info(result);

方法5 利用while循环在数组里push分隔符,首尾不加

// 利用while循环在数组里push分隔符
var result="",
  index = 0,
  len = str.length,
  i = len-1,
  arr = str.split("");
while(len-index>0){
  len>=index&&len-index!==len && arr.splice(len-index,0,",");
  index+=3;
  i-=4;
};
console.log(arr.join(""));

78、load和ready区别

document.ready:

是ready,表示文档结构已经加载完成 不包含图片等非文字媒体文件 只要html标签结构加载完毕就可以;

document.load:

是onload,指示页面包含图片等文件在内的所有元素都加载完成。

1、概念

2、作用

document.ready:

在DOM加载完成后就可以可以对DOM进行操作。

一般情况一个页面响应加载的顺序是,域名解析-加载html-加载js和css-加载图片等其他信息。
那么Dom Ready应该在“加载js和css”和“加载图片等其他信息”之间,就可以操作Dom了。

document.load:

在document文档加载完成后就可以可以对DOM进行操作,document文档包括了加载图片等其他信息。

那么Dom Load就是在页面响应加载的顺序中的“加载图片等其他信息”之后,就可以操作Dom了。

3、加载顺序

document.ready:

文档加载的顺序:域名解析-->加载HTML-->加载JavaScript和CSS-->加载图片等非文字媒体文件。

只要<img>标签加载完成,不用等该图片加载完成,就可以设置图片的属性或样式等。

在原生JavaScript中没有Dom ready的直接方法。

document.load:

文档加载的顺序:域名解析-->加载HTML-->加载JavaScript和CSS-->加载图片等非文字媒体文件。

DOM load在加载图片等非文字媒体文件之后,表示在document文档加载完成后才可以对DOM进行操作,document文档包括了加载图片等非文字媒体文件。

例如,需要等该图片加载完成,才可以设置图片的属性或样式等。

在原生JavaScript中使用onload事件。

79、自定义事件

自定义事件,就是自己定义事件类型,自己定义事件处理函数。

我们平时操作dom时经常会用到onclick、onmousemove等浏览器特定行为的事件类型。

封装is自定义事件基本的构思:

var eventTarget = {
  addEvent: function(){
    //添加事件
  },
  fireEvent: function(){
    //触发事件
  },
  removeEvent: function(){
    //移除事件
  }
};

在js默认事件中事件类型以及对应的执行函数是一一对应的,但是自定义事件,需要一个映射表来建立两者之间的联系。

如: 这样每个类型可以处理多个事件函数

handlers = {
      "type1":[
            "fun1",
            "fun2",
            // "..."
         ],
       "type2":[
            "fun1",
            "fun2"
             // "..."
         ]
         //"..."
}

代码实现:

function EventTarget(){
    //事件处理程序数组集合
    this.handlers={};
}

//自定义事件的原型对象
EventTarget.prototype={
    //设置原型构造函数链
    constructor:EventTarget,
    //注册给定类型的事件处理程序
    //type->自定义事件类型,如click,handler->自定义事件回调函数
    addEvent:function(type,handler){
        //判断事件处理函数中是否有该类型事件
        if(this.handlers[type]==undefined){
            this.handlers[type]=[];
        }
        this.handlers[type].push(handler);
    },

    //触发事件
    //event为一个js对象,属性中至少包含type属性。
    fireEvent:function(event){
        //模拟真实事件的event
        if(!event.target){
            event.target=this;
        }
        //判断是否存在该事件类型
        if(this.handlers[event.type] instanceof Array){
            var items=this.handlers[event.type];
            //在同一事件类型下可能存在多个事件处理函数,依次触发
            //执行触发
            items.forEach(function(item){
                item(event);
            })
        }
    },

    //删除事件
    removeEvent:function(type,handler){
        //判断是否存在该事件类型
        if(this.handlers[type] instanceof Array){
            var items=this.handlers[type];
            //在同一事件类型下可能存在多个处理事件
            for(var i=0;i<items.length;i++){
                if(items[i]==handler){
                    //从该类型的事件数组中删除该事件
                    items.splice(i,1);
                    break;
                }
            }
        }
    }

}

//调用方法
function fun(){
    console.log('执行该方法');
}
function fun1(obj){
    console.log('run '+obj.min+'s');
}
var target=new EventTarget();
target.addEvent("run",fun);//添加事件
target.addEvent("run",fun1);//添加事件

target.fireEvent({type:"run",min:"30"});//执行该方法   123

target.removeEvent("run",fun);//移除事件

target.fireEvent({type:"run",min:"20"});//123

为什么要把方法添加到对象原型上?

在构造函数中加属性,在原型中加方法。

将属性和方法都写在构造函数里是没有问题的,但是每次进行实例化的过程中,要重复创建功能不变的方法。

由于方法本质上是函数,其实也就是在堆内存中又新建了一个对象空间存放存储函数,造成了不必要的资源浪费。

在本身添加会导致每次对象实例化时代码被复制,都需要申请一块内存存放该方法。

写一个EventEmitter类,包括on()、off()、once()、emit()方法once():为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。

class EventEmitter{
            constructor(){
                this.handlers={};
            }
            on(type,fn){
                if(!this.handlers[type]){
                    this.handlers[type]=[];
                }
                this.handlers[type].push(fn);
                return this;
            }
            off(type,fn){
                let fns=this.handlers[type];
                for(let i=0;i<fns.length;i++){
                    if(fns[i]==fn){
                        fns.splice(i,1);
                        break;
                    }
                }
                return this;
            }
            emit(...args){
                let type=args[0];
                let params=[].slice.call(args,1);
                let fn=this.handlers[type];
                fn.forEach((item)=>{
                    item.apply(this,params);//执行函数
                })
                return this;
            }
            once(type,fn){
                let wrap=(...args)=>{
                    fn.apply(this,args);//执行事件后删除
                    this.off(type,wrap);
                }
                this.on(type,wrap);//再添加上去
                return this;
            }
        }
         let emitter=new EventEmitter();
    function fun1(){
        console.log('fun1');
    }
    function fun2(){
        console.log('fun2');
    }
    function fun3(){
        console.log('fun3');
    }
    emitter.on('TEST1',fun1).on('TEST2',fun2).emit('TEST1').once('TEST2',fun3);
    emitter.emit("TEST2");

80、setTimeout实现setInterval

setTimeout() :在指定的毫秒数后调用函数或计算表达式,只执行一次。
setInterval() :按照指定的周期(以毫秒计)来调用函数或计算表达式。方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。

思路是使用递归函数,不断地去执行setTimeout从而达到setInterval的效果,看代码

function mySetInterval(fn, millisec){
  function interval(){
    setTimeout(interval, millisec);
    fn();
  }
  setTimeout(interval, millisec)
}

这个mySetInterval函数有一个叫做interval的内部函数,它通过setTimeout来自动被调用,在interval中有一个闭包,调用了回调函数并通过setTimeout再次调用了interval。

一个更好的实现我们再增加一个额外的参数用来标明代码执行的次数

function mySetInterval(fn, millisec,count){
  function interval(){
    if(typeof count===‘undefined’||count-->0){
      setTimeout(interval, millisec);
      try{
        fn()
      }catch(e){
        count = 0;
        throw e.toString();
      }
    }
  }
  setTimeout(interval, millisec)
}

81、避免回调地狱

使用 async await 配合 promise 是 解决回调地狱的终极方法

async/await特点

1, async/await更加语义化,async 是“异步”的简写,async function 用于申明一个 function 是异步的;await,可以认为是async wait的简写, 用于等待一个异步方法执行完成;

2, async/await是一个用同步思维解决异步问题的方案(等结果出来之后,代码才会继续往下执行)

3, 可以通过多层 async function 的同步写法代替传统的callback嵌套

async function语法

1, 自动将常规函数转换成Promise,返回值也是一个Promise对象

2, 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数

3, 异步函数内部可以使用await

await语法

1, await 放置在Promise调用之前,await 强制后面点代码等待,直到Promise对象resolve,得到resolve的值作为await表达式的运算结果

2. await只能在async函数内部使用,用在普通函数里就会报错

函数形式

function timeout(ms) {

  return new Promise((resolve, reject) => {

    setTimeout(() => {reject('error')}, ms);  //reject模拟出错,返回error

  });

}

async function asyncPrint(ms) {

  try {

     console.log('start');

     await timeout(ms);  //这里返回了错误

     console.log('end');  //所以这句代码不会被执行了

  } catch(err) {

     console.log(err); //这里捕捉到错误error

  }

}

82、callee和caller的作用

caller返回一个函数的引用,这个函数调用了当前的函数;callee放回正在执行的函数本身的引用,它是arguments的一个属性

callercaller返回一个函数的引用,这个函数调用了当前的函数。使用这个属性要注意:1 这个属性只有当函数在执行时才有用2 如果在javascript程序中,函数是由顶层调用的,则返回null

functionName.caller: functionName是当前正在执行的函数。
var a = function() {
    alert(a.caller);
}
var b = function() {
    a();
}
b();

上面的代码中,b调用了a,那么a.caller返回的是b的引用,结果如下:

var b = function() {
    a();
}

如果直接调用a(即a在任何函数中被调用,也就是顶层调用),返回null:

var a = function() {
    alert(a.caller);
}
var b = function() {
    a();
}
//b();
a();
输出结果:
null

callee

callee放回正在执行的函数本身的引用,它是arguments的一个属性

使用callee时要注意:calleecallee放回正在执行的函数本身的引用,它是arguments的一个属性使用callee时要注意:

1 这个属性只有在函数执行时才有效

2 它有一个length属性,可以用来获得形参的个数,因此可以用来比较形参和实参个数是否一致,即比较arguments.length是否等于arguments.callee.length

3 它可以用来递归匿名函数。

var a = function() {
    alert(arguments.callee);
}
var b = function() {
    a();
}
b();


a在b中被调用,但是它返回了a本身的引用,结果如下:

var a = function() {
    alert(arguments.callee);
}

83、统计字符串中字母个数或统计最多的字母数

统计字母出现的次数

function count( str ){
    var obj={};
    for(var i=0;i<str.length; i++){
        if(obj[ str[i] ]==undefined){
            //对象初始化;如果key在对象中找不到,那么会返回undefined,反向思维
            obj[ str[i] ]= 1;
        } else{
            obj[ str[i] ]++;
        }
    }

    //取出各个字母和它的个数,作为一个新对象保存在obj对象中
    return obj;

}

cosnle.log( count( "shhkfahkahsadhadskhdskdha" ) );

统计字符出现次数最多的字母

function allProMax(obj){
    var mm="";
    for(var m in obj){
        if(mm==""){
            mm=new Object();
            mm[m]=obj[m];
        }else{
            for(var j in mm){
               if(mm[j]<obj[m]){
                   //清空原来的内容
                   mm=new Object();
                   //放入新的内容
                   mm[m]=obj[m];
               }
            }
        }
    }
    return mm ;
}

console.log( allProMax(count()) )

84、面对对象和面向过程的区别

一、面向对象与面向过程的区别
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

可以拿生活中的实例来理解面向过程与面向对象,例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。

如果是面向对象的设计思想来解决问题。面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。

上述的内容是从网上查到的,觉得这个例子非常的生动形象,我就写了下来,现在就应该理解了他俩的区别了吧,其实就是两句话,面向对象就是高度实物抽象化、面向过程就是自顶向下的编程!

二、面向对象的特点
在了解其特点之前,咱们先谈谈对象,对象就是现实世界存在的任何事务都可以称之为对象,有着自己独特的个性

1, 概念 对 具有相同特性的一类事物的抽象描述

2, 组成 属性 和 方法

3, 模板 构造函数

4, 特点 封装 继承 多态

属性用来描述具体某个对象的特征。比如小志身高180M,体重70KG,这里身高、体重都是属性。
面向对象的思想就是把一切都看成对象,而对象一般都由属性+方法组成!

属性属于对象静态的一面,用来形容对象的一些特性,方法属于对象动态的一面,咱们举一个例子,小明会跑,会说话,跑、说话这些行为就是对象的方法!所以为动态的一面, 我们把属性和方法称为这个对象的成员!

类:具有同种属性的对象称为类,是个抽象的概念。比如“人”就是一类,期中有一些人名,比如小明、小红、小玲等等这些都是对象,类就相当于一个模具,他定义了它所包含的全体对象的公共特征和功能,对象就是类的一个实例化,小明就是人的一个实例化!我们在做程序的时候,经常要将一个变量实例化,就是这个原理!我们一般在做程序的时候一般都不用类名的,比如我们在叫小明的时候,不会喊“人,你干嘛呢!”而是说的是“小明,你在干嘛呢!”

面向对象有三大特性,分别是封装性、继承性和多态性,这里小编不给予太多的解释,因为在后边的博客会专门总结的!

三、面向过程与面向对象的优缺点
很多资料上全都是一群很难理解的理论知识,整的小编头都大了,后来发现了一个比较好的文章,写的真是太棒了,通俗易懂,想要不明白都难!

用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。

蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。

蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。

到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。

盖浇饭的好处就是"菜"“饭"分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是"可维护性"比较好,“饭” 和"菜"的耦合度比较低。蛋炒饭将"蛋”“饭"搅和在一起,想换"蛋”"饭"中任何一种都很困难,耦合度很高,以至于"可维护性"比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。

我们最后简单总结一下

面向过程

优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展


面向对象

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低

85、eval

eval()是全局对象的一个函数属性。

eval()的参数是一个字符串。如果字符串表示的是表达式,eval()会对表达式进行求值。如果参数表示一个或多个JavaScript语句, 那么eval()就会执行这些语句。注意不要用eval()来执行一个四则运算表达式;因为 JavaScript 会自动为四则运算求值并不需要用eval来包裹。

这里的四则运算是指数学上的运算,如:3 + 4 * 4 / 6。注意这里面并没有变量,只是单纯的数学运算,这样的运算式并不需要调用eval来计算,直接在代码中计算就可以。其实即便带有变量,JavaScript也是可以直接计算的,但是如果你现在只想声明一个带有变量的表达式,但是想稍后进行运算(你有可能在声明这个带有变量的运算式之后还有可能对里面的变量进行修改),就可以使用eval。
如果要将算数表达式构造成为一个字符串,你可以用eval()在随后对其求值。比如,假如你有一个变量 x ,你可以通过一个字符串表达式来对涉及x的表达式延迟求值,将 “3 * x + 2”,存储为变量,然后在你的脚本后面的一个地方调用eval()。

如果eval()的参数不是字符串,eval()将会将参数原封不动的返回。在下面的例子中,字符串构造器被指定,eval()返回了字符串对象而不是对字符串求值。

// 返回了包含"2 + 2"的字符串对象
eval(new String("2 + 2"));

// returns 4
eval("2 + 2");


eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您可能会利用最终在用户机器上运行恶意方部署的恶意代码,并导致您失去您的网页或者扩展程序的权限。更重要的是,第三方代码可以看到某一个eval()被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的Function就是不容易被攻击的。

eval()的运行效率也普遍的比其他的替代方案慢,因为他会调用js解析器,即便现代的JS引擎中已经对此做了优化。

在常见的案例中我们都会找更安全或者更快的方案去替换他

86、proxy

一、proxy

proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截

var proxy = new Proxy(target, handler);
new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为

var target = {
  name: 'poetries'
};
var logHandler = {
  get: function(target, key) {
    console.log(`${key} 被读取`);
    return target[key];
  },
  set: function(target, key, value) {
    console.log(`${key} 被设置为 ${value}`);
    target[key] = value;
  }
}
var targetWithLog = new Proxy(target, logHandler);

targetWithLog.name; // 控制台输出:name 被读取
targetWithLog.name = 'others'; // 控制台输出:name 被设置为 others

console.log(target.name); // 控制台输出: others
targetWithLog 读取属性的值时,实际上执行的是 logHandler.get :在控制台输出信息,并且读取被代理对象 target 的属性。
在 targetWithLog 设置属性值时,实际上执行的是 logHandler.set :在控制台输出信息,并且设置被代理对象 target 的属性的值
// 由于拦截函数总是返回35,所以访问任何属性都得到35
var proxy = new Proxy({}, {
get: function(target, property) {
  return 35;
}
});

proxy.time // 35
proxy.name // 35
proxy.title // 35
Proxy 实例也可以作为其他对象的原型对象

var proxy = new Proxy({}, {
get: function(target, property) {
  return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35

proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截

Proxy的作用

对于代理模式 Proxy 的作用主要体现在三个方面

拦截和监视外部对对象的访问降低函数或类的复杂度在复杂操作前对操作进行校验或对所需资源进行管理

二、Proxy所能代理的范围--handler

实际上 handler 本身就是ES6所新设计的一个对象.它的作用就是用来 自定义代理对象的各种可代理操作 。它本身一共有13中方法,每种方法都可以代理一种操作.其13种方法如下

// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。
handler.getPrototypeOf()

// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。
handler.setPrototypeOf()


// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。
handler.isExtensible()


// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。
handler.preventExtensions()

// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。
handler.getOwnPropertyDescriptor()


// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。
andler.defineProperty()


// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。
handler.has()

// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。
handler.get()


// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。
handler.set()

// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。
handler.deleteProperty()

// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。
handler.ownKeys()

// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。
handler.apply()


// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。
handler.construct()

三、Proxy场景

3.1 实现私有变量

var target = {
   name: 'poetries',
   _age: 22
}

var logHandler = {
  get: function(target,key){
    if(key.startsWith('_')){
      console.log('私有变量age不能被访问')
      return false
    }
    return target[key];
  },
  set: function(target, key, value) {
     if(key.startsWith('_')){
      console.log('私有变量age不能被修改')
      return false
    }
     target[key] = value;
   }
}
var targetWithLog = new Proxy(target, logHandler);

// 私有变量age不能被访问
targetWithLog.name;

// 私有变量age不能被修改
targetWithLog.name = 'others';
在下面的代码中,我们声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey

var api = {
    _apiKey: '123abc456def',
    /* mock methods that use this._apiKey */
    getUsers: function(){},
    getUser: function(userId){},
    setUser: function(userId, config){}
};

// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey;
api._apiKey = '987654321';
很显然,约定俗成是没有束缚力的。使用 ES6 Proxy 我们就可以实现真实的私有变量了,下面针对不同的读取方式演示两个不同的私有化方法。第一种方法是使用 set / get 拦截读写请求并返回 undefined:

let api = {
    _apiKey: '123abc456def',
    getUsers: function(){ },
    getUser: function(userId){ },
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});

// 以下操作都会抛出错误
console.log(api._apiKey);
api._apiKey = '987654321';
第二种方法是使用 has 拦截 in 操作

var api = {
    _apiKey: '123abc456def',
    getUsers: function(){ },
    getUser: function(userId){ },
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    has(target, key) {
        return (RESTRICTED.indexOf(key) > -1) ?
            false :
            Reflect.has(target, key);
    }
});

// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);

for (var key in api) {
    if (api.hasOwnProperty(key) && key === "_apiKey") {
        console.log("This will never be logged because the proxy obscures _apiKey...")
    }
}

3.2 抽离校验模块

让我们从一个简单的类型校验开始做起,这个示例演示了如何使用 Proxy 保障数据类型的准确性

let numericDataStore = {
    count: 0,
    amount: 1234,
    total: 14
};

numericDataStore = new Proxy(numericDataStore, {
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("Properties in numericDataStore can only be numbers");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

// 抛出错误,因为 "foo" 不是数值
numericDataStore.count = "foo";

// 赋值成功
numericDataStore.count = 333;
如果要直接为对象的所有属性开发一个校验器可能很快就会让代码结构变得臃肿,使用 Proxy 则可以将校验器从核心逻辑分离出来自成一体

function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`);
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    });
}

const personValidators = {
    name(val) {
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && val > 18;
    }
}
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}

const bill = new Person('Bill', 25);

// 以下操作都会报错
bill.name = 0;
bill.age = 'Bill';
bill.age = 15;
通过校验器和主逻辑的分离,你可以无限扩展 personValidators 校验器的内容,而不会对相关的类或函数造成直接破坏。更复杂一点,我们还可以使用 Proxy 模拟类型检查,检查函数是否接收了类型和数量都正确的参数

let obj = {
    pickyMethodOne: function(obj, str, num) { /* ... */ },
    pickyMethodTwo: function(num, obj) { /*... */ }
};

const argTypes = {
    pickyMethodOne: ["object", "string", "number"],
    pickyMethodTwo: ["number", "object"]
};

obj = new Proxy(obj, {
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...args) {
            var checkArgs = argChecker(key, args, argTypes[key]);
            return Reflect.apply(value, target, args);
        };
    }
});

function argChecker(name, args, checkers) {
    for (var idx = 0; idx < args.length; idx++) {
        var arg = args[idx];
        var type = checkers[idx];
        if (!arg || typeof arg !== type) {
            console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
        }
    }
}

obj.pickyMethodOne();
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo", {});
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodOne(123, {});

3.3 访问日志

对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用 Proxy 充当中间件的角色,轻而易举实现日志功能

let api = {
    _apiKey: '123abc456def',
    getUsers: function() { /* ... */ },
    getUser: function(userId) { /* ... */ },
    setUser: function(userId, config) { /* ... */ }
};

function logMethodAsync(timestamp, method) {
    setTimeout(function() {
        console.log(`${timestamp} - Logging ${method} request asynchronously.`);
    }, 0)
}

api = new Proxy(api, {
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...arguments) {
            logMethodAsync(new Date(), key);
            return Reflect.apply(value, target, arguments);
        };
    }
});

api.getUsers();

3.4 预警和拦截

假设你不想让其他开发者删除 noDelete 属性,还想让调用 oldMethod 的开发者了解到这个方法已经被废弃了,或者告诉开发者不要修改 doNotChange 属性,那么就可以使用 Proxy 来实现

let dataStore = {
    noDelete: 1235,
    oldMethod: function() {/*...*/ },
    doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];

dataStore = new Proxy(dataStore, {
    set(target, key, value, proxy) {
        if (NOCHANGE.includes(key)) {
            throw Error(`Error! ${key} is immutable.`);
        }
        return Reflect.set(target, key, value, proxy);
    },
    deleteProperty(target, key) {
        if (NODELETE.includes(key)) {
            throw Error(`Error! ${key} cannot be deleted.`);
        }
        return Reflect.deleteProperty(target, key);

    },
    get(target, key, proxy) {
        if (DEPRECATED.includes(key)) {
            console.warn(`Warning! ${key} is deprecated.`);
        }
        var val = target[key];

        return typeof val === 'function' ?
            function(...args) {
                Reflect.apply(target[key], target, args);
            } :
            val;
    }

});

// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";
delete dataStore.noDelete;
dataStore.oldMethod();

3.5 过滤操作

某些操作会非常占用资源,比如传输大文件,这个时候如果文件已经在分块发送了,就不需要在对新的请求作出相应(非绝对),这个时候就可以使用 Proxy 对当请求进行特征检测,并根据特征过滤出哪些是不需要响应的,哪些是需要响应的。下面的代码简单演示了过滤特征的方式,并不是完整代码,相信大家会理解其中的妙处

let obj = {
    getGiantFile: function(fileId) {/*...*/ }
};

obj = new Proxy(obj, {
    get(target, key, proxy) {
        return function(...args) {
            const id = args[0];
            let isEnroute = checkEnroute(id);
            let isDownloading = checkStatus(id);
            let cached = getCached(id);

            if (isEnroute || isDownloading) {
                return false;
            }
            if (cached) {
                return cached;
            }
            return Reflect.apply(target[key], target, args);
        }
    }

});

猜你喜欢

转载自blog.csdn.net/sdasadasds/article/details/132041731