前端小姐姐五万字面试宝典:JS基础
1.继承
- 1、原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类构造函数传参。
- 2、构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类,构造继承可以向父类传递参数,可以实现多继承,通过call多个父类对象。但是构造继承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服用,每个子类都有父类实例函数的副本,影响性能
- 3、实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制调用方法,不管是new 子类()还是子类()返回的对象具有相同的效果,缺点是实例是父类的实例,不是子类的实例,不支持多继承
- 4、拷贝继承:特点:支持多继承,缺点:效率较低,内存占用高(因为要拷贝父类的属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
- 5、组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
- 6、寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
2.this指向
(1).this 指向有哪几种
- 1.默认绑定:全局环境中,this默认绑定到window。
- 2.隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象。
- 3.隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。显式绑定:通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。
- 4.new绑定:如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。
- 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
- 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
- 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。
(2).改变函数内部 this 指针的指向函数(bind,apply,call的区别)
- 1.apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。
- 2.call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。
- 3.bind除了返回是函数以外,它的参数和call一样。
(3).箭头函数
- 1.箭头函数没有this,所以需要通过查找作用域链来确定this的值,这就意味着如果箭头函数被非箭头函数包含,this绑定的就是最近一层非箭头函数的this,
- 2.箭头函数没有自己的arguments对象,但是可以访问外围函数的arguments对象
- 3.不能通过new关键字调用,同样也没有new.target值和原型
3.数据类型
(1).基本数据类型
Undefined、Null、Boolean、Number 、String、Symbol
(2).symbol
- 1.语法:
// 不能用 new let s = Symbol() // 可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 let s1 = Symbol('foo'); let s2 = Symbol('bar'); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // "Symbol(foo)" s2.toString() // "Symbol(bar)" 复制代码
- 2.作用:定义一个独一无二的值
- 1.用作对象的属性名
- 1.不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。 - 2.
Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 - 3.
Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
- 1.不会出现在
- 2.用于定义一组常量
log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn') }; 复制代码
- 1.用作对象的属性名
- 3.类型转换:
- 1.转成字符串
String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)' 复制代码
- 2.转成布尔值
Boolean(sym) !sym 复制代码
- 3.不能转成数字
- 4.不能与其他类型的值进行运算
let sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}` // TypeError: can't convert symbol to string 复制代码
- 1.转成字符串
- 4.属性:Symbol.prototype.description
- 5.Symbol.for(),Symbol.keyFor()
- 1.在全局环境中登记 Symbol 值。之后不会再重复生成
(3).如何判断类型
typeof(),instanceof,Object.prototype.toString.call()
- 1.
typeof
操作符- 1."undefined"——如果这个值未定义;
- 2."boolean"——如果这个值是布尔值;
- 3."string"——如果这个值是字符串;
- 4."number"——如果这个值是数值;
- 5."object"——如果这个值是对象或 null;
- 6."function"——如果这个值是函数。
- 7."symbol"——es6新增的symbol类型
- 2.
instanceof
:用来判断对象是不是某个构造函数的实例。会沿着原型链找的 - 3.
Object.prototype.toString.call()
var toString = Object.prototype.toString; toString.call(new Date); // [object Date] toString.call(new String); // [object String] toString.call(Math); // [object Math] toString.call([]); // [Object Array] toString.call(new Number) // [object Number] toString.call(true) // [object Boolean] toString.call(function(){}) // [object Function] toString.call({}) // [object Object] toString.call(new Promise(() => {})) // [object Promise] toString.call(new Map) // [object Map] toString.call(new RegExp) // [object RegExp] toString.call(Symbol()) // [object Symbol] toString.call(function *a(){}) // [object GeneratorFunction] toString.call(new DOMException()) // [object DOMException] toString.call(new Error) // [object Error] toString.call(undefined); // [object Undefined] toString.call(null); // [object Null] // 还有 WeakMap、 WeakSet、Proxy 等 复制代码
(4).判断是否是数组
- 1.
Array.isArray(arr)
- 2.
Object.prototype.toString.call(arr) === '[Object Array]'
- 3.
arr instanceof Array
- 4.
array.constructor === Array
(5).字符串转数字
parseInt(string, radix)
4.CallBack Hell
大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。
我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可 以复用,且没有重复代码的开销。
(1).Promise 为什么以及如何用于解决控制反转信任问题
Promise 的实现可以看这里
Promise 这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可靠更合理。 通过把回调的控制反转反转回来,我们把控制权放在了一个可信任的系统(Promise)中, 这种系统的设计目的就是为了使异步编码更清晰。Promise 并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任 的中介机制。
- 调用回调过早;
- 这个问题主要就是担心代码是否会引入类似 Zalgo 这样的副作用(参见第 2 章)。在这类问 题中,一个任务有时同步完成,有时异步完成,这可能会导致竞态条件。
根据定义,Promise 就不必担心这种问题,因为即使是立即完成的 Promise(类似于 new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。
也就是说,对一个 Promise 调用 then(..) 的时候,即使这个 Promise 已经决议,提供给 then(..) 的回调也总会被异步调用(对此的更多讨论,请参见 1.5 节)。
- 这个问题主要就是担心代码是否会引入类似 Zalgo 这样的副作用(参见第 2 章)。在这类问 题中,一个任务有时同步完成,有时异步完成,这可能会导致竞态条件。
- 调用回调过晚(或不被调用);
- 和前面一点类似,Promise 创建对象调用 resolve(..) 或 reject(..) 时,这个 Promise 的 then(..) 注册的观察回调就会被自动调度。可以确信,这些被调度的回调在下一个异步事 件点上一定会被触发(参见 1.5 节)。
- 回调未调用
- 首先,没有任何东西(甚至 JavaScript 错误)能阻止 Promise 向你通知它的决议(如果它 决议了的话)。如果你对一个 Promise 注册了一个完成回调和一个拒绝回调,那么 Promise 在决议时总是会调用其中的一个。
- 但是,如果 Promise 本身永远不被决议呢?即使这样,Promise 也提供了解决方案,其使用 了一种称为竞态的高级抽象机制:
- 调用回调次数过多;
- Promise 的定义方式使得它只能被决议一次。如果出于某种 原因,Promise 创建代码试图调用 resolve(..) 或 reject(..) 多次,或者试图两者都调用, 那么这个 Promise 将只会接受第一次决议,并默默地忽略任何后续调用。
- 由于 Promise 只能被决议一次,所以任何通过 then(..) 注册的(每个)回调就只会被调 用一次。
- 未能传递所需的环境和参数;
- Promise 至多只能有一个决议值(完成或拒绝)。
如果你没有用任何值显式决议,那么这个值就是 undefined,这是 JavaScript 常见的处理方 式。但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或 拒绝)回调。
- Promise 至多只能有一个决议值(完成或拒绝)。
- 吞掉可能出现的错误和异常。
- 如果拒绝一个 Promise 并给出一个理由(也就是一个出错消息),这个值就会被传给拒绝回调
(2).promise、generator、async/await
- promise
- 优点:解决了回调地狱的问题
- 缺点:无法取消 Promise ,错误需要通过回调函数来捕获
- generator
- 生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤
- async/await
- 优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
- 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
5.加载
(1).异步加载js的方法
- defer:只支持IE如果您的脚本不会改变文档的内容,可将 defer 属性加入到
<script>
标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。 - async:HTML5 属性,仅适用于外部脚本;并且如果在IE中,同时存在defer和async,那么defer的优先级比较高;脚本将在页面完成时执行。
(2).图片的懒加载和预加载
- 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
- 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
6.事件
(1).事件流
HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
IE只支持事件冒泡。
(2).什么是事件监听
addEventListener()
方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为
element.addEventListener(event, function, useCapture)
;
- 第一个参数是事件的类型(如 "click" 或 "mousedown").
- 第二个参数是事件触发后调用的函数。
- 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。
target.addEventListener(type, listener, options: EventListenerOptions);
target.addEventListener(type, listener, useCapture: boolean);
target.addEventListener(type, listener, useCapture: boolean, wantsUntrusted: boolean ); // Gecko/Mozilla only
复制代码
interface EventListenerOptions {
capture?: boolean // 表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发
once?: boolean // 表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除
passive?: boolean // 设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告
}
复制代码
(3). mouseover 和 mouseenter 的区别
- mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
- mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
(4). 事件委托以及冒泡原理
简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。
举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
(5). 事件代理在捕获阶段的实际应用
可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。
7.跨域
(1).CORS
CORS(Cross-Origin Resource Sharing,跨源资源共享) 背后的基本思想,就是使用自定义的 HTTP 头部 让浏览器与服务器进行沟通。
比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在 发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端 口),以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:
Origin: http://www.nczonline.net
如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源
信息(如果是公共资源,可以回发"*")。例如:
Access-Control-Allow-Origin: http://www.nczonline.net
如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器 会处理请求。注意,请求和响应都不包含 cookie 信息。
(2).IE
微软在 IE8 中引入了 XDR(XDomainRequest)类型。以下是 XDR 与 XHR 的一些不同之 处。
- cookie 不会随请求发送,也不会随响应返回。
- 只能设置请求头部信息中的 Content-Type 字段。
- 不能访问响应头部信息。
- 只支持GET和POST请求。
(3).其他浏览器
通过 XMLHttpRequest 对象实现了对 CORS 的原生支持
- 不能使用 setRequestHeader()设置自定义头部。
- 不能发送和接收 cookie。
- 调用 getAllResponseHeaders()方法总会返回空字符串。
(4).JSONP
微信公众号:世界上有意思的事
function handleResponse(response){
alert("You’re at IP address " + response.ip + ", which is in " +
response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse"; document.body.insertBefore(script, document.body.firstChild);
复制代码
- JSON只支持get,因为script标签只能使用get请求;
- JSONP需要后端配合返回指定格式的数据。
(5). 代理
起一个代理服务器,实现数据的转发
(6).利用 iframe
- window.postMessage
- Cross Frame(aba)
- window.name
lovelock.coding.me/javascript/…
(7).window.postMessage
只支持到IE8及以上的IE浏览器,其他现代浏览器当然没有问题。
(8). child 与 parent 通信
不受同源策略的限制
- 给接收数据的一方添加事件绑定:
addEventListener('message', receiveMessage);
- 发送数据的一方拿到接收数据一方的window:
targetWindow.postMessage("Welcome to unixera.com", "http://iframe1.unixera.com");
(9).chilid 与 child 通信
有跨域问题,只适合站内不同子域间的通信(设置document.domain为同一级域名)
(10).Cross Frame
这是一个通用的方法,简单来说是A iframe包含B iframe,在B iframe中调用了相关的接口,完成调用之后获取到结果,location.href
到和A iframe位于同一个域的C iframe,在C iframe中调用A iframe中定义的方法,将B iframe中获取的结果作为参数传到要跳转的url后,在C iframe中通过location.search
变量来获取变量。
(11).window.name
window
对象的name
属性是一个很特殊的属性,在设定了window.name
之后,执行location.href
跳转,window.name
属性仍然不会发生变化,可以通过这种方式实现变量的传递。
8.Ajax
(1).实现一个Ajax
微信公众号:世界上有意思的事
var xhr = new XMLHttpRequest()
// 必须在调用 open()之前指定 onreadystatechange 事件处理程序才能确保跨浏览器兼容性
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status ==== 304) {
console.log(xhr.responseText)
} else {
console.log('Error:' + xhr.status)
}
}
}
// 第三个参数表示异步发送请求
xhr.open('get', '/api/getSth', true)
// 参数为作为请求主体发送的数据
xhr.send(null)
复制代码
(2).Ajax状态
- 未初始化。尚未调用 open()方法。
- 启动。已经调用 open()方法,但尚未调用 send()方法。
- 发送。已经调用 send()方法,但尚未接收到响应。
- 接收。已经接收到部分响应数据。
- 完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
(3).将原生的 ajax 封装成 promise
微信公众号:世界上有意思的事
const ajax = (url, method, async, data) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
// 已经接收到全部响应数据,而且已经可以在客户端使用了
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else if (xhr.status > 400) {
reject('发生错误')
}
}
}
xhr.open(url, method, async)
xhr.send(data || null)
})
}
复制代码
9.垃圾回收
找出那些不再继续使用的变 量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作。
(1).标记清除
先所有都加上标记,再把环境中引用到的变量去除标记。剩下的就是没用的了
(2).引用计数
跟踪记录每 个值被引用的次数。清除引用次数为0的变量 ⚠️会有循环引用问题 。循环引用如果大量存在就会导致内存泄露。
10.eval是什么
eval 方法就像是一个完整的 ECMAScript 解析器,它只接受一个参数,即要执行的 ECMAScript (或JavaScript) 字符串
- 1.性能差:引擎无法在编译时对作用域查找进行优化
- 1.JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符。
- 2.无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底 是什么。最悲观的情况是如果出现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简 单的做法就是完全不做任何优化。
- 2.欺骗作用域:但在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。
11.监听对象属性的改变
(一).ES5 中
微信公众号:世界上有意思的事
Object.defineProperty(user,'name',{
set:function(key,value){
// 这也是 Vue 的原理
}
})
复制代码
(二). ES6 中
微信公众号:世界上有意思的事
var user = new Proxy({}, {
set:function(target,key,value,receiver){
}
})
复制代码
可以监听动态增加的属性。例如 user.id = 1
12.实现一个私有变量
- 1.配置属性
obj={ name: 'xujiahui', getName:function(){ return this.name } } object.defineProperty(obj,"name",{ //不可枚举不可配置 }); 复制代码
- 2.代码
微信公众号:世界上有意思的事
function product(){
var name='xujiahui';
this.getName=function(){
return name;
}
}
var obj=new product();
复制代码
13.操作符
(1).==
和===
、以及Object.is
的区别
- 1.
==
- 1.会进行强制类型转换(!=也是)
- 2.在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:
- 3.如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false 转换为 0,而true 转换为 1
- 4.如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
- 5.如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法,用得到的基本类型值按照前面的规则进行比较; 这两个操作符在进行比较时则要遵循下列规则。
- 6.null 和 undefined 是相等的。
- 7.要比较相等性之前,不能将 null 和 undefined 转换成其他任何值。
- 8.如果有一个操作数是 NaN,则相等操作符返回 false,而不相等操作符返回 true。重要提示⚠️:即使两个操作数都是 NaN,相等操作符也返回 false;因为按照规则,NaN 不等于 NaN。
- 9.如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true;否则,返回 false。
- 2.
===
:全等于,不转换 - 3.
Object.is
- 1.也不会进行强制类型转换。
- 2.与
===
有以下几点不同:- 1.
+0===-0
,Object.is(+0, -0)
为 false - 2.
NaN !== NaN
,Object.is(NaN, NaN)
为 true
- 1.
(2).new 操作符做了哪些事情
用 new 操作符调用构造函数实际上会经历以下 4 个步骤:
- 1.创建一个新对象;
- 2.将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 3.执行构造函数中的代码(为这个新对象添加属性);
- 4.返回新对象。
- 5.将构造函数的prototype关联到实例的__proto__
14.数组
(1).数组常用方法
push(),pop(),shift(),unshift(),splice(),sort(),reverse(),map()等
(2).数组去重
要注意的是对象咋去重
- 1.双重循环
每次插入一个元素的时候都和前面的每个元素比较一下
var array = [1, 1, '1', '1']; function unique(array) { // res用来存储结果 var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++ ) { if (array[i] === res[j]) { break; } } // 如果array[i]是唯一的,那么执行完循环,j等于resLen if (j === resLen) { res.push(array[i]) } } return res; } console.log(unique(array)); // [1, "1"] 复制代码
- 2.
indexOf
原理和双重循环是一样的
var array = [1, 1, '1']; function unique(array) { var res = []; for (var i = 0, len = array.length; i < len; i++) { var current = array[i]; if (res.indexOf(current) === -1) { res.push(current) } } return res; } console.log(unique(array)); 复制代码
- 3.排序后去重
对于排好序的数组,可以将每个元素与前一个比较
var array = [1, 1, '1']; function unique(array) { var res = []; var sortedArray = array.concat().sort(); var seen; for (var i = 0, len = sortedArray.length; i < len; i++) { // 如果是第一个元素或者相邻的元素不相同 if (!i || seen !== sortedArray[i]) { res.push(sortedArray[i]) } seen = sortedArray[i]; } return res; } console.log(unique(array)); 复制代码
- 4.Object 键值对
把每一个元素存成 object 的 key。例如
['a']
,存成{'a': true}
var array = [1, 2, 1, 1, '1']; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ return obj.hasOwnProperty(item) ? false : (obj[item] = true) }) } console.log(unique(array)); // [1, 2] 复制代码
我们可以发现,是有问题的,因为 1 和 '1' 是不同的,但是这种方法会判断为同一个值,这是因为对象的键值只能是字符串,所以我们可以使用
typeof item + item
拼成字符串作为 key 值来避免这个问题:var array = [1, 2, 1, 1, '1']; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true) }) } console.log(unique(array)); // [1, 2, "1"] 复制代码
然而,即便如此,我们依然无法正确区分出两个对象,比如 {value: 1} 和 {value: 2},因为
typeof item + item
的结果都会是object[object Object]
,不过我们可以使用 JSON.stringify 将对象序列化:var array = [{value: 1}, {value: 1}, {value: 2}]; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ console.log(typeof item + JSON.stringify(item)) return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true) }) } console.log(unique(array)); // [{value: 1}, {value: 2}] 复制代码
- 5.ES6 Set去重
function unique(array) { return Array.from(new Set(array)); } 复制代码
function unique(array) { return [...new Set(array)]; } 复制代码
- 6.ES6 Map
function unique (arr) { const seen = new Map() return arr.filter((a) => !seen.has(a) && seen.set(a, 1)) }
作者:何时夕
链接:https://juejin.im/post/5e91b01651882573716a9b23
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。