front-end-interview-questions
front-end-interview-questions copied to clipboard
【手写实现】JavaScrip头部高频手写实现面试题目汇总
手写instanceof
实现的原理
function myInstanceof(left,right){
// 首先判断基本数据类型
if(typeof left!=='object' || left === null) return false
// getProtypeOf是Object对象自带的api 获取参数的原型对象
let proto = Object.getPrototypeOf(left)
while(true){
if(proto===null)return false
if(proto === right.prototype) return true // 找到相同的原型对象 返回true
proto = Object.getPrototypeOf(proto)
}
}
console.log(myInstanceof(123,Number)) // false
console.log(myInstanceof(new Number(123),Number)) // true
- 本文首次发布时间:2020年12月02日
- 建议阅读时长:1H
- 建议阅读对象:初中级前端工程师、JavaScript爱好者
- 文章没有涉及
Promise
涉及较少数组
相关,后续会更新数组专题,以及异步编程
专题,当然啦也有不少常见的你喜欢的手写实现没有出现,说不定哪天你回来看看就更新上去了呦
-
「前端厚说」CSS头部高频面试题及参考答案 修改于2020年12月02号
-
「前端厚说」HTML头部高频面试题及参考答案 发布于2020年11月30日
更新记录
- 2020年12月08日 增加
form表单提交
- 2020年12月26日 增加
原生ajax的封装
前言
这是前端厚说
大系列第三篇正式的文章。作为前端开发者,那么html
我们抛砖引玉了一篇,那么css 我们也简单分享了一篇,自然而然要轮到Js
了,接下直接来看看“所谓的”手写实现。其实在整理学习的过程中,我发现了一个道理:就是为什么这些是老生常谈的问题,或者说话题。不难发现这些正是身为一个前端开发者最基本的东西,且不说各种的框架,像this
呀 作用域链
闭包
继承
等等其实在我们实际的开发中或者说那些框架中无处不在。告别焦虑且不贩卖焦虑 。另:案例代码在文末
手写实现ajax
请求
/* 封装ajax函数
* @param {string}opt.type http连接的方式,包括POST和GET两种方式
* @param {string}opt.url 发送请求的url
* @param {boolean}opt.async 是否为异步请求,true为异步的,false为同步的
* @param {object}opt.data 发送的参数,格式为对象类型
* @param {function}opt.success ajax发送并接收成功调用的回调函数
*/
ajax (opt) {
opt = opt || {};
opt.method = opt.method.toUpperCase() || 'POST';
opt.url = opt.url || '';
opt.async = opt.async || true;
opt.data = opt.data || null;
opt.success = opt.success || function () { };
let xmlHttp = null;
if (XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
} else {
xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');
}
this.XHR = xmlHttp;
var params = [];
for (var key in opt.data) {
params.push(key + '=' + opt.data[key]);
}
var postData = params.join('&');
if (opt.method.toUpperCase() === 'POST') {
xmlHttp.open(opt.method, opt.url, opt.async);
xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xmlHttp.send(postData);
} else if (opt.method.toUpperCase() === 'GET') {
xmlHttp.open(opt.method, opt.url + '?' + postData, opt.async);
xmlHttp.send(null);
}
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
opt.success(xmlHttp.responseText);
}
};
}
手写实现form
表单提交
function submit(){
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if(xhr.status>=200&&xhr.status<300 || xhr.status ==304){
console.log(xhr.responseText)
}
}
}
xhr.open('post','https://jsonplaceholder.typicode.com/todos')
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded')
let form = document.getElementById("user-info");
xhr.send(serialize(form));
}
手写实现防抖函数(debounce/防抖动)
- 第一个防抖函数
let num =1
const container = document.querySelector('.container')
// 基本函数内容
function commonFnCon(){
container.innerHTML = num++
}
function baseShowNumber(){
commonFnCon()
console.log(this) // <div>num</div>
}
function firstShowNumber(){
commonFnCon()
console.log(this) // this 指的是 window
}
function firstDebounce(fn,wait){
let timerId = null;
return function (){
if(timerId) clearTimeout(timerId)
// 一句话:一件事情触发了,1s内不再触发此事件
timerId = setTimeout(fn,wait)
}
}
// container.onmousemove = baseShowNumber
container.onmousemove = firstDebounce(firstShowNumber,1000)
- 第二个防抖函数解决this的指向问题
let num =1
const container = document.querySelector('.container')
// 基本函数内容
function commonFnCon(){
container.innerHTML = num++
}
function baseShowNumber(){
commonFnCon()
console.log(this) // <div>num</div>
}
function secShowNumber(){
commonFnCon()
console.log('sec',this) // this 指的是 window
}
function secDebounce(fn,wait){
let timerId = null;
return function (){
let ctx = this
console.log('ctx',ctx) // 此时的ctx 就是baseShowNumber中的<div>num</div>
if(timerId) clearTimeout(timerId)
// 一句话:一件事情触发了,1s内不再触发此事件
timerId = setTimeout(()=>{
// 接下来就是把当前环境的this绑定到事件函数(这里指的是baseShowNumber)上
// 并执行该事件函数
fn.apply(ctx)
},wait)
}
}
// container.onmousemove = baseShowNumber
container.onmousemove = secDebounce(secShowNumber,1000)
- 第三个防抖函数修复事件对象为undefined的问题
let num =1
const container = document.querySelector('.container')
// 基本函数内容
function commonFnCon(){
container.innerHTML = num++
}
function baseShowNumber(e){
commonFnCon()
console.log(e) // MouseEvent
console.log(this) // <div>num</div>
}
function thirdShowNumber(e){
commonFnCon()
}
function thirdDebounce(fn,wait){
let timerId = null;
return function (){
let ctx = this
let args = arguments
console.log('ctx',ctx) // 此时的ctx 就是baseShowNumber中的<div>num</div>
console.log('args',arguments) // 此时的arguments 刚好是个伪数组,其中包含事件对象
if(timerId) clearTimeout(timerId)
// 一句话:一件事情触发了,1s内不再触发此事件
timerId = setTimeout(()=>{
// 接下来就是把当前环境的this绑定到事件函数(这里指的是baseShowNumber)上
// 并执行该事件函数
fn.apply(ctx,args)
},wait)
}
}
// container.onmousemove = baseShowNumber
container.onmousemove = thirdDebounce(thirdShowNumber,1000)
- 小结
上述实现的依然是不够完整的,接下来自己再延伸探索吧,贴上我们企业项目中的防抖函数
const debounce = (fn, delay, isImmediate) => {
var timer = null;
return function() {
var that = this;
var args = [].slice.call(arguments);
var callNow = !timer && isImmediate;
if(timer) clearTimeout(timer);
// 非立即执行
timer = setTimeout(function() {
timer = null;
if(!isImmediate) fn.apply(that, args);
}, delay);
// 立即执行
if(callNow) fn.apply(that, args);
}
};
export {
debounce
}
- 业务场景
- 主要是点击按钮刷新操作,用于防止频繁刷新
- 还有就是form表单的验证(异步调接口的验证场景)
手写实现节流函数(throttle)
- 认识节流
节流是每隔一段时间,只执行一次事件,防抖是一件事情触发了,1s内不再触发此事件
function throttle(func, wait) {
let timerId = null
let now = 0
return function(){
let context = this;
let args = arguments;
if(!timerId){
timerId = setTimeout(()=>{
timerId = null
func.apply(context,args)
},wait)
}
}
}
手写实现深浅拷贝(深浅克隆)
背景
深浅拷贝 是面试中的
明星话题
首先明确一点我们接下来探讨的都是引用类型,一般我们拷贝的也就是像数组对象这种较为复杂的数据类型
什么是引用类型,我下边举个例子,也就是说 你和弟弟都是家里的人 一旦你弟弟 改变了你家的布局,那么你俩拿钥匙回到家看到的是一样的,都是改变后的样子 也就是说 你俩是一家人(这里就不用女朋友举例了)
如何切断数据,就是重新拷贝一份 浅拷贝就是表面的拷贝 深拷贝就是无限层级的拷贝下去
浅拷贝
先来说说浅拷贝,浅拷贝:创建一个新对象,有旧对象原始属性值(基本类型,拷贝的是基本类型;引用类型,便是内存地址)一份精确拷贝 其中一个对象地址改变,相互影响(也就是说虽然拷贝过了,但是还是会相互影响的)
let obj1 = {
name: "张三",
age: 18,
};
let obj2 = obj1;
obj2.name = "李四";
console.log(obj1.name); // 此时第一个对象的name属性就被改掉了
我们发现,我们通过一个简单的 赋值操作来完成这一操作
那我们是不是也可以通过一个简单的函数来实现,那就是
const shallow = (target) => {
let obj = {};
for (let prop in target) {
obj[prop] = target[prop];
}
};
从数组来看的话
let targetArr = [{name:'oldName'},"1", "1", 1, 1, true, true, undefined, undefined, null, null,]
let resultArr = targetArr.concat()
resultArr[0]['name'] = 'newName'
console.log('old',targetArr) // 此时 targetArr 的第一个元素的 name 也被修改了
console.log('new',resultArr)
具体实现
function firstShallowClone(target){
if(typeof target !== 'object') return
let result = Array.isArray(target) ? [] :{}
for(let k in target){
if(target.hasOwnProperty(k)){
result[k] = target[k]
}
}
return result
}
深拷贝
深拷贝的核心思路便是 拷贝加递归 也就是说当对象的某一个属性还是个对象的时候,我们需要对之进一步拷贝,从内存完整拷贝,在堆中重新开启区间,对象地址改变不会影响
第一个深拷贝:通过JSON
的两个API
- JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象
- JSON.stringify() 方法将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串
实现 JavaScript 中的深拷贝,有一种非常取巧的方式 —— JSON.stringify,
let obj = {
name: "yayxs",
fav: [
{
type: "play"
}
],
friend: {
name: "wanghuahua"
}
};
const objStr = JSON.stringify(obj) // deepClone.js:16 {"name":"yayxs","fav":[{"type":"play"}],"friend":{"name":"wanghuahua"}}
const objCopy = JSON.parse(objStr)
objCopy.fav.splice(0,1)
console.log(obj['fav']) // [{}]
console.log(objCopy['fav']) //[]
但是如果单个元素是函数的话,我们来试一下
let fnArr = [
()=>{
console.log(1)
},
()=>{
console.log(2)
}
]
console.log(JSON.parse(JSON.stringify(fnArr))); // [null, null]
第二个深拷贝:递归拷贝
判断一下属性值的类型,当目前属性值的类型是个对象的时候,然后递归克隆,
function firstDeepClone(target){
// 如果是 值类型 或 null,则直接return
if(typeof target !== 'object' ||target===null ) {
return target
}
// 结果对象
let res = target instanceof Array ? [] : {};
for(let key in res){
if(obj.hasOwnProperty(key)){
// 首先判断当前key 所对应的属性值是否是个引用类型
if(typeof obj[key] === 'object'){
res[key] = firstDeepClone(obj[key])
}else{
res[key] = obj[key]
}
}
}
}
手写实现 JavaScript 中的 New 操作符
延伸的面试题
根据new操作符
相关的知识点一般会 延伸出以下的面试题 ,面试官你是否有很多问号
- 问题一:new 之后都做了些什么??
- 问题二:能否手写 new 操作符原理??
- 问题三:通过 new 的方式创建对象和通过字面量创建有什么区别
- 创建一个空的简单 JavaScript 对象(即
{}
);- 链接该对象(即设置该对象的构造函数)到另一个对象 ;
- 将步骤 1 新创建的对象作为
this
的上下文 ;- 如果该函数没有返回对象,则返回
this
。
以上 4 条是MDN
上关于 new 操作符(或者说关键字)的面试,简单的来体验下利用构造函数来new
一个对象
function Person(name, age) {
console.log("this", this);
this.name = name;
this.age = age;
}
// 然后在**构造函数添加原型方法**
Person.prototype.height = 180;
Person.prototype.sayName = function() {
console.log(this.name);
};
let p = new Person("yayxs", 20);
console.log(p.name); // yayxs
console.log(p.age);
20;
p.sayName(); // yayxs
console.log(p.__proto__ === Person.prototype); // 对象p(实例)的原型属性指向构造函数的原型,
既然我们通过自定义,其使用的方式大体跟new
是一样的。
// ------ 使用new的时候
const p = myNew Person('yayxs',20) // 其返回的结果是一个对象
// ---------
第一版的 myNew
大体思路是声明一个对象,取出当前的构造函数,以及参数,让新对象的原型属性指向构造函数的原型,然后调用构造函数,传入对象的参数
function myNew() {
let obj = new Object(),
[constructor, ...args] = [...arguments];
obj.__proto__ = constructor.prototype;
constructor.apply(obj, args);
return obj;
}
第二版的myNew
经过上文的简单案例我们可以得知,
-
new
一个构造函数得到一个对象,它的原型属性(也就是** proto **)与该构造函数的原型是全等 -
new
通过构造函数Persion
创建出来的实例可以访问到构造函数中的属性,就像这样console.log(xiaoMing.name); // 小明
-
言简意赅:new 出来的实例对象通过原型链和构造函数联系起来
构造函数说白了也是一个函数,那是函数就可以有返回值
function Person(name) {
this.name = name;
// return 1; // 返回内部新创建的对象
// return "1"; // 返回内部新创建的对象
// return null; // 返回内部新创建的对象
// return undefined; // 返回内部新创建的对象
// return {}; // {} // 直接返回
return function() {}; // 直接返回
return [1]; // [1] // 直接返回
}
let p = new Person("李四");
console.log(p);
有了给构造函数返回一个值得想法,那就通过不同的数据类型
进行测试得出结论
- 不同的数据类型返回的效果是不一样的,像数字 1 字符串”1“ ,返回的依然是内部创建的对象
- 那如果返回一个对象({})或者说数组([]) 都会直接返回回去
小结
也就是说,构造函数一般不需要return
- 返回一般的数据类型吧,不起作用
- 返回对象吧, new 的意义又何在呢
function myNew(){
let obj = new Object(),
[constructor,...args] = [...arguments]
obj.__proto__ = constructor.prototype;
let res = constructor.apply(obj,args)
return = typeof res === 'object' ? res : obj;
}
手写一个自己的 myNew 小结
如果自己实现一个 new 的话,首先要满足它的几点效果
-
一个构造函数会返回一个对象,那函数里就应该有对象
let obj = {};
-
并将其
__proto__
属性指向构造函数的prototype
属性obj.__proto__ = constructor.prototype;
-
调用构造函数,绑定 this
constructor.apply(obj, args);
-
返回原始值需要忽略,返回对象需要正常处理
res instanceof Object ? res : obj;
箭头函数使用new
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
- 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
手写实现 call 与 apply
前言
函数可以被传递、用作对象等让我们先看一段代码,函数虽然将调用传递给原始的方法,但是上下文this
却会存在不见的地方,这也就引出我们的call()
方法,call
是什么黑魔法,不过是一个内置函数方法,使用的方式是 add.call()
,并且add
函数自动执行输出结果 3
// 参数context 指定的this值
// arg1 参数一
// arg2 参数二
func.call(context, arg1, arg2, ...)
function add(a, b) {
console.log("add函数中this", this);
console.log(a + b);
}
add(1, 2); // this指向window
add.call({ name: "yayxs" }, 1, 2); // this指向传入的 {name:'yayxs'}
实现第一版 call
这时候我们把传入的对象抽离出来
let o = {
name: "yayxs",
};
function sayName() {
console.log(this.name);
}
sayName.call(o); // yayxs
Function.prototype.myCall = function(ctx) {
console.log(this) // 其中this 就是sayName 这个函数
console.log(ctx) // {name: "yayxs"}
ctx.tempFunc = this
ctx.tempFunc()
delete ctx.tempFunc
};
sayName.myCall(o,'参数一','参数二') // 理论上输出 yayxs
实现第二版 call
我们上述的myCall
传入的参数一和参数二并没有参与感,再完善一下
Function.prototype.myCall = function(ctx) {
console.log(this) // 其中this 就是sayName 这个函数
console.log(ctx) // {name: "yayxs"}
console.log('arguments',arguments)
let tempArgs = [];// 用来存放参数
for(let i=1,len=arguments.length;i<len;i++){
console.log(arguments[i]) // 第一遍循环体 输出参数一 第二遍循环体 参数二
tempArgs.push('arguments[' + i + ']');
}
console.log(tempArgs);
ctx.tempFunc = this
// ctx.tempFunc()
let evalScript = 'ctx.tempFunc(' + tempArgs +')'
eval(evalScript);
delete ctx.tempFunc
};
手动实现 apply
// ---------------- 实现myApply
Function.prototype.myApply = function(ctx,arr){
ctx.tempFunc = this
let result
if(!arr){
result = ctx.tempFunc() // 直接执行
}else{
let args = []
for (let i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('ctx.tempFunc(' + args + ')')
}
delete ctx.tempFunc
return result
}
总结
总体来说,call
apply
函数的作用都是用来改变this
的指向。目前的 js 还存在回调函数这一现象,尤其在框架中一些异步回调
也是十分的常见,难免this
会迷失方向既有不同也有相似,倘若自己手写代码实现call
与 apply
- 获取被绑定的函数
- 被绑定的函数追加到劫持替换的对象
- 被绑定的函数追加到劫持替换的对象
方法名 | 作用 | 是否自动执行 | 参数列表 |
---|---|---|---|
call | 改变 this 指向 | 自动执行函数 | 一般列表 |
apply | 改变 this 指向 | 自动执行函数 | 数组形式 |
手写实现bind 函数
在手写实现bind
之前,我们先来回忆一下bind
的使用场景,我们就说在react
框架中好了,
class App extends React.Component {
constructor() {
super();
this.state = {
num: 0,
};
}
// 1 合成事件中的setState
handleClick() {
console.log(this.state);
this.setState({ num: this.state.num + 1 }); // 合成事件执行完, state 并没有更新 造成所谓的 异步 try 代码块执行完事之后
console.log(this.state);
}
componentDidUpdate() {
console.log(this.state.num);
}
render() {
return (
<>
{this.state.num}
// 可以看到我们使用 bind函数来绑定this
<button onClick={this.handleClick.bind(this)}>按钮</button>
</>
);
}
}
export default App;
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函 数的参数,供调用时使用。但是我们需要事先说一下
bind
是需要Polyfill
的。因为大部分的浏览器都实现了内置的Function.prototype.bind
的实现,也有些是不支持的
const user = {
name:'yayxs',
}
function showYourself(){
console.log(this.name)
}
const result = showYourself.bind(user)
第一个bind
const user = {
name: "yayxs",
};
function showYourself(sex) {
console.log(this.name);
console.log(sex)
}
let resultFn;
resultFn = showYourself.bind(user);
// console.log(resultFn)
Function.prototype.myFirstBind = function(ctx) {
console.log(ctx); // user {name:'yayxs'}
let _this = this;
// 第一步返回一个函数
return function() {
// _this 此时是showYourself函数
return _this.apply(ctx);
};
};
resultFn = showYourself.myFirstBind(user);
// console.log(resultFn)
第二个bind
Function.prototype.secBind = function() {
let self = this, // 保存原来的函数
context = [].shift.call(arguments); // 需要绑定的this的上下文
args = [].slice.call(arguments); // 剩余的参数转成数组
return function() {
// 返回一个新的函数
// 执行新的函数的时候,会把之前传入的context当做新函数体内的this 并且组合两次分别差UN额逇参数 作为新函数的参数
return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
};
};
手写实现 String.prototpye.trim() 方法
ECMAScript 在所有字符串上都提供了 trim()方法。这个方法会创建字符串的一个副本,删除前、后所有空格符,再返回结果。 由于 trim()返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。
为简化子字符串替换操作,ECMAScript 提供了 replace()方法。这个方法接收两个参数,第一个参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,
用 replace 结合正则实现清除字符串两边空格的方法 保留两侧的空格,而清除内部的空格
String.prototype.trim
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/gm, '');
}
}
手写实现EventBus/EventEmitter
// eventBus.js 文件
import Vue from "vue";
const EventBus = new Vue(); // 本质上也是 Vue 实例
export default EventBus;
// main.js
import EventBus from "eventBus.js";
Vue.prototype.EventBus = EventBus;
// 派发事件
this.$EventBus.$emit("sendVal", "派发事件的");
// 监听事件
this.$EventBus.$on("sendVal", (val) => {
console.log(val);
});
class EventEmitter {
constructor() {
this.handles = new Map(); // 存储实践回调之间的关系
}
on(evtName, cb) {
if (!this.handles.has(evtName)) {
this.handles.set(evtName, []);
}
this.handles[evtName].push(cb);
}
emit(evtName, ...args) {
if (this.handles.has(evtName)) {
for (let i = 0, len = this.handles[evtName].length; i < len; i++) {
this.handles[evtName][cb](...args);
}
}
}
off(evtName, cb) {
const cbs = this.handles[evtName];
const idx = cbs.indexOf(cb);
if (idx !== -1) {
cbs.splice(idx, 1);
}
}
once(evtName, cb) {
const warp = (...args) => {
cb(...args);
this.off(evtName, warp);
};
this.on(evtName, warp);
}
}
手写实现数组去重的方法
要想搞明白数组去重的各种方案,第一步要做的事就是什么是重复的元素,先来看一段代码结果
/**
* 谈到数组去重,几乎是面试必备的一道开胃菜
* 要想数组去重,第一件事就是有一个数组
*/
// 首先第一步
console.log(1 === 1); // true
console.log("1" === "1"); // true
console.log("true" === "true"); // true
console.log(false === false); // true
console.log(undefined === undefined); // true
console.log(null === null); // true
console.log(NaN === NaN); // false
console.log({} === {}); // false
console.log([] === []); // false
接着第二步:准备含有重复元素的目标数组
let targetArr = ["1", "1", 1, 1, true, true, undefined, undefined, null, null];
console.log(targetArr);
第三步:写去重方法,这时候才是正是写方法的时候
/**
* desc 第一种方案 双层for循环
*/
function unique1(arr) {
let result = []; // 结果数组
for (let i = 0, len = arr.length; i < len; i++) {
for (var j = 0, resLen = result.length; j < resLen; j++) {
if (arr[i] === result[j]) {
break;
}
}
if (j === result.length) {
result.push(arr[i]);
}
}
return result;
}
/**
* desc 第二种方案 indexOf
*/
const unique2 = (arr) => {
let result = [];
for (let i = 0, len = arr.length; i < len; i++) {
if (result.indexOf(arr[i]) === -1) {
// 在结果数组中没有找到元素
result.push(arr[i]);
}
}
return result;
};
/**
* desc 第三种方案 先给数组排序结合 sort函数
*/
const unique3 = (target) => {
target.sort();
let result = [target[0]]; // 取出第一个元素
for (let i = 1; i < target.length; i++) {
target[i] !== target[i - 1] && result.push(target[i]); // 当前项和它的前一项不同的时候,才添加进结果数组
}
return result;
};
/**
* desc 第四种方案 使用filter 函数结合 indexof
*/
const unique4 = (target) => {
return target.filter((item, index) => {
return target.indexOf(item) === index; // 数组的下标与检索的下标一致
});
};
/**
* desc 第五种方案 使用对象的键值对 结合obj.hasOwnProperty()
*/
const unique5 = (target) => {
let obj = {}; // 初始化一个空的对象
let result = new Array();
result = target.filter((item, index) =>
// typeof item + item 主要是考虑到 对象的key 数值1 会被搞成 '1'
obj.hasOwnProperty(typeof item + item)
? false
: (obj[typeof item + item] = true)
);
return result;
};
/**
* desc 第六种方案 使用ES6的新语法Set
*
*/
const unique6 = (target) => {
// Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
let x = new Set(target); // Set { '1', 1, true, null }
return [...x]; // 转为数组
};
/**
* desc 第七种方案 使用ES6的Array.from
*
*/
const unique7 = (target) => {
// Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
let x = new Set(target); // Set { '1', 1, true, null }
return Array.from(x); // 转为数组
};
/**
* desc 第八种方案 使用哈希表
*
*/
const unique8 = (target) => {
let result = []; // 结果数组
let hash = {};
for (let ele of target) {
if (!hash[ele]) {
// hash 中没有数组中的元素
result.push(ele); // 把元素放进去
hash[ele] = true; // hash中已经存在了标记为true 下次循环就不会接着往结果数组中放相同的元素了
}
}
return result;
};
/**
* desc 第九种方案 使用Map 类型
* 缺点 hash中存在相同的 key 就不往下找了, 但 '1' 和 1 是不同的元素
* 请同学自行测试吧哈哈~
*/
const unique9 = (target) => {
let map = new Map(); // 初始化 map
let result = new Array(); // 初始化 数组
for (let i = 0; i < target.length; i++) {
if (map.has(target[i])) {
map.set(target[i], true);
} else {
map.set(target[i], false);
result.push(target[i]);
}
}
return result;
};
/**
* desc 第十种方案 双层for循环变体
*
*
*/
const unique10 = (target) => {
// let result = [];
for (let i = 0; i < target.length; i++) {
for (let j = i + 1; j < target.length; j++) {
if (target[i] === target[j]) {
// 如果两项元素相同的话,则从目标元素中删除一个
target.splice(j, 1);
// splice 会改变原数组,所以相关的长度都要减去一
i--;
j--;
}
}
}
return target;
};
/**
* desc 第十一种方案 数据的遍历结合includes
*
*
*/
const unique11 = (target) => {
let result = [];
for (let ele of target) {
// 其中 ele 是每一目标元素中每一元素
!result.includes(ele) && result.push(ele); // 如果结果数组中没有ele就添加进去
}
return result;
};
/**
* desc 第十二种方案 数据reduce
*
*
*/
const unique12 = (target) => {
return target.reduce((previousValue, currentValue, currentIndex) => {
return previousValue.includes(currentValue)
? previousValue
: [...previousValue, currentValue];
}, []);
};
console.log(unique1(targetArr)); // ["1", 1, true, undefined, null]
console.log(unique2(targetArr)); // ["1", 1, true, undefined, null]
console.log(unique3(targetArr)); // ["1", 1, null, true, undefined]
console.log(unique4(targetArr)); // ["1", 1, null, true, undefined]
console.log(unique5(targetArr)); // ["1", 1, null, true, undefined]
手写实现输出由*号组成的三角形
/**
* * 1----1
* ** 2----2
* *** 3----3
* **** 4----4
*/
for (let row = 0; row < 4; row++) { // 行数
for (let num = 0; num < row; num++) {
document.write("*");
}
document.write("<br />");
}
for(let row = 4;row>0;row--){
for(let num=0;num<row;num++){
document.write("*");
}
document.write("<br />");
}
- 作者:洋小洋同学
- 上文所及代码案例可在 ……/demos/written
- 备用浏览地址 ……debounce.html
- 参考
- 冴羽的博客
- 《JavaScript设计模式与开发实践》
- 如果对你有帮助的话多谢点赞评论,后续希望分享什么专题?