实现apply办法
思路: 利用this
的上下文个性。apply
其实就是改一下参数的问题
Function.prototype.myApply = function(context = window, args) { // this-->func context--> obj args--> 传递过去的参数 // 在context上加一个惟一值不影响context上的属性 let key = Symbol('key') context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的办法 // let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组 let result = context[key](...args); // 这里和call传参不一样 // 革除定义的this 不删除会导致context属性越来越多 delete context[key]; // 返回后果 return result;}
// 应用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2]) //arguments[1]
转化为驼峰命名
var s1 = "get-element-by-id"// 转化为 getElementById
var f = function(s) { return s.replace(/-\w/g, function(x) { return x.slice(1).toUpperCase(); })}
前端手写面试题具体解答
将数字每千分位用逗号隔开
数字有小数版本:
let format = n => { let num = n.toString() // 转成字符串 let decimals = '' // 判断是否有小数 num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals let len = num.length if (len <= 3) { return num } else { let temp = '' let remainder = len % 3 decimals ? temp = '.' + decimals : temp if (remainder > 0) { // 不是3的整数倍 return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp } else { // 是3的整数倍 return num.slice(0, len).match(/\d{3}/g).join(',') + temp } }}format(12323.33) // '12,323.33'
数字无小数版本:
let format = n => { let num = n.toString() let len = num.length if (len <= 3) { return num } else { let remainder = len % 3 if (remainder > 0) { // 不是3的整数倍 return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') } else { // 是3的整数倍 return num.slice(0, len).match(/\d{3}/g).join(',') } }}format(1232323) // '1,232,323'
实现AJAX申请
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新以后网页的对应局部,而不必刷新整个网页。
创立AJAX申请的步骤:
- 创立一个 XMLHttpRequest 对象。
- 在这个对象上应用 open 办法创立一个 HTTP 申请,open 办法所须要的参数是申请的办法、申请的地址、是否异步和用户的认证信息。
- 在发动申请前,能够为这个对象增加一些信息和监听函数。比如说能够通过 setRequestHeader 办法来为申请增加头信息。还能够为这个对象增加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变动时会触发onreadystatechange 事件,能够通过设置监听函数,来解决申请胜利后的后果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接管实现,这个时候能够通过判断申请的状态,如果状态是 2xx 或者 304 的话则代表返回失常。这个时候就能够通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置实现后,最初调用 sent 办法来向服务器发动申请,能够传入参数作为发送的数据体。
const SERVER_URL = "/server";let xhr = new XMLHttpRequest();// 创立 Http 申请xhr.open("GET", SERVER_URL, true);// 设置状态监听函数xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当申请胜利时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); }};// 设置申请失败时的监听函数xhr.onerror = function() { console.error(this.statusText);};// 设置申请头信息xhr.responseType = "json";xhr.setRequestHeader("Accept", "application/json");// 发送 Http 申请xhr.send(null);
手写 Promise.then
then
办法返回一个新的 promise
实例,为了在 promise
状态发生变化时(resolve
/ reject
被调用时)再执行 then
里的函数,咱们应用一个 callbacks
数组先把传给then的函数暂存起来,等状态扭转时再调用。
那么,怎么保障后一个 **then**
里的办法在前一个 **then**
(可能是异步)完结之后再执行呢? 咱们能够将传给 then
的函数和新 promise
的 resolve
一起 push
到前一个 promise
的 callbacks
数组中,达到承前启后的成果:
- 承前:以后一个
promise
实现后,调用其resolve
变更状态,在这个resolve
里会顺次调用callbacks
里的回调,这样就执行了then
里的办法了 - 启后:上一步中,当
then
里的办法执行实现后,返回一个后果,如果这个后果是个简略的值,就间接调用新promise
的resolve
,让其状态变更,这又会顺次调用新promise
的callbacks
数组里的办法,周而复始。。如果返回的后果是个promise
,则须要等它实现之后再触发新promise
的resolve
,所以能够在其后果的then
里调用新promise
的resolve
then(onFulfilled, onReject){ // 保留前一个promise的this const self = this; return new MyPromise((resolve, reject) => { // 封装前一个promise胜利时执行的函数 let fulfilled = () => { try{ const result = onFulfilled(self.value); // 承前 return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后 }catch(err){ reject(err) } } // 封装前一个promise失败时执行的函数 let rejected = () => { try{ const result = onReject(self.reason); return result instanceof MyPromise? result.then(resolve, reject) : reject(result); }catch(err){ reject(err) } } switch(self.status){ case PENDING: self.onFulfilledCallbacks.push(fulfilled); self.onRejectedCallbacks.push(rejected); break; case FULFILLED: fulfilled(); break; case REJECT: rejected(); break; } }) }
留神:
- 间断多个
then
里的回调办法是同步注册的,但注册到了不同的callbacks
数组中,因为每次then
都返回新的promise
实例(参考下面的例子和图) - 注册实现后开始执行构造函数中的异步事件,异步实现之后顺次调用
callbacks
数组中提前注册的回调
实现一个call
call做了什么:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定this到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);//实现一个call办法:Function.prototype.myCall = function(context) { //此处没有思考context非object状况 context.fn = this; let args = []; for (let i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } context.fn(...args); let result = context.fn(...args); delete context.fn; return result;};
实现千位分隔符
// 保留三位小数parseToMoney(1234.56); // return '1,234.56'parseToMoney(123456789); // return '123,456,789'parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) { num = parseFloat(num.toFixed(3)); let [integer, decimal] = String.prototype.split.call(num, '.'); integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,'); return integer + '.' + (decimal ? decimal : '');}
正则表达式(使用了正则的前向申明和反前向申明):
function parseToMoney(str){ // 仅仅对地位进行匹配 let re = /(?=(?!\b)(\d{3})+$)/g; return str.replace(re,','); }
实现斐波那契数列
// 递归function fn (n){ if(n==0) return 0 if(n==1) return 1 return fn(n-2)+fn(n-1)}// 优化function fibonacci2(n) { const arr = [1, 1, 2]; const arrLen = arr.length; if (n <= arrLen) { return arr[n]; } for (let i = arrLen; i < n; i++) { arr.push(arr[i - 1] + arr[ i - 2]); } return arr[arr.length - 1];}// 非递归function fn(n) { let pre1 = 1; let pre2 = 1; let current = 2; if (n <= 2) { return current; } for (let i = 2; i < n; i++) { pre1 = pre2; pre2 = current; current = pre1 + pre2; } return current;}
Function.prototype.bind
Function.prototype.bind = function(context, ...args) { if (typeof this !== 'function') { throw new Error("Type Error"); } // 保留this的值 var self = this; return function F() { // 思考new的状况 if(this instanceof F) { return new self(...args, ...arguments) } return self.apply(context, [...args, ...arguments]) }}
判断对象是否存在循环援用
循环援用对象原本没有什么问题,然而序列化的时候就会产生问题,比方调用JSON.stringify()
对该类对象进行序列化,就会报错: Converting circular structure to JSON.
上面办法能够用来判断一个对象中是否已存在循环援用:
const isCycleObject = (obj,parent) => { const parentArr = parent || [obj]; for(let i in obj) { if(typeof obj[i] === 'object') { let flag = false; parentArr.forEach((pObj) => { if(pObj === obj[i]){ flag = true; } }) if(flag) return true; flag = isCycleObject(obj[i],[...parentArr,obj[i]]); if(flag) return true; } } return false;}const a = 1;const b = {a};const c = {b};const o = {d:{a:3},c}o.c.b.aa = a;console.log(isCycleObject(o)
查找有序二维数组的目标值:
var findNumberIn2DArray = function(matrix, target) { if (matrix == null || matrix.length == 0) { return false; } let row = 0; let column = matrix[0].length - 1; while (row < matrix.length && column >= 0) { if (matrix[row][column] == target) { return true; } else if (matrix[row][column] > target) { column--; } else { row++; } } return false;};
二维数组斜向打印:
function printMatrix(arr){ let m = arr.length, n = arr[0].length let res = [] // 左上角,从0 到 n - 1 列进行打印 for (let k = 0; k < n; k++) { for (let i = 0, j = k; i < m && j >= 0; i++, j--) { res.push(arr[i][j]); } } // 右下角,从1 到 n - 1 行进行打印 for (let k = 1; k < m; k++) { for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) { res.push(arr[i][j]); } } return res}
Object.is
Object.is
解决的次要是这两个问题:
+0 === -0 // trueNaN === NaN // false
const is= (x, y) => { if (x === y) { // +0和-0应该不相等 return x !== 0 || y !== 0 || 1/x === 1/y; } else { return x !== x && y !== y; }}
模板引擎实现
let template = '我是{{name}},年龄{{age}},性别{{sex}}';let data = { name: '姓名', age: 18}render(template, data); // 我是姓名,年龄18,性别undefined
function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找以后模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的构造 } return template; // 如果模板没有模板字符串间接返回}
手写 bind 函数
bind 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 保留以后函数的援用,获取其余传入参数值。
- 创立一个函数返回
- 函数外部应用 apply 来绑定函数调用,须要判断函数作为构造函数的状况,这个时候须要传入以后函数的 this 给 apply 调用,其余状况都传入指定的上下文对象。
// bind 函数实现Function.prototype.myBind = function(context) { // 判断调用对象是否为函数 if (typeof this !== "function") { throw new TypeError("Error"); } // 获取参数 var args = [...arguments].slice(1), fn = this; return function Fn() { // 依据调用形式,传入不同绑定值 return fn.apply( this instanceof Fn ? this : context, args.concat(...arguments) ); };};
实现字符串翻转
在字符串的原型链上增加一个办法,实现字符串翻转:
String.prototype._reverse = function(a){ return a.split("").reverse().join("");}var obj = new String();var res = obj._reverse ('hello');console.log(res); // olleh
须要留神的是,必须通过实例化对象之后再去调用定义的办法,不然找不到该办法。
实现防抖函数(debounce)
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则从新计时。
那么与节流函数的区别间接看这个动画实现即可。
手写简化版:
// 防抖函数const debounce = (fn, delay) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); };};
实用场景:
- 按钮提交场景:避免屡次提交按钮,只执行最初提交的一次
- 服务端验证场景:表单验证须要服务端配合,只执行一段间断的输出事件的最初一次,还有搜寻联想词性能相似
生存环境请用lodash.debounce
实现prototype继承
所谓的原型链继承就是让新实例的原型等于父类的实例:
//父办法function SupperFunction(flag1){ this.flag1 = flag1;}//子办法function SubFunction(flag2){ this.flag2 = flag2;}//父实例var superInstance = new SupperFunction(true);//子继承父SubFunction.prototype = superInstance;//子实例var subInstance = new SubFunction(false);//子调用本人和父的属性subInstance.flag1; // truesubInstance.flag2; // false
函数珂里化
指的是将一个承受多个参数的函数 变为 承受一个参数返回一个函数的固定模式,这样便于再次调用,例如f(1)(2)
经典面试题:实现add(1)(2)(3)(4)=10;
、 add(1)(1,2,3)(2)=9;
function add() { const _args = [...arguments]; function fn() { _args.push(...arguments); return fn; } fn.toString = function() { return _args.reduce((sum, cur) => sum + cur); } return fn;}
实现类的继承
类的继承在几年前是重点内容,有n种继承形式各有优劣,es6遍及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深刻了解的去看红宝书即可,咱们目前只实现一种最现实的继承形式。
function Parent(name) { this.parent = name}Parent.prototype.say = function() { console.log(`${this.parent}: 你打篮球的样子像kunkun`)}function Child(name, parent) { // 将父类的构造函数绑定在子类上 Parent.call(this, parent) this.child = name}/** 1. 这一步不必Child.prototype =Parent.prototype的起因是怕共享内存,批改父类原型对象就会影响子类 2. 不必Child.prototype = new Parent()的起因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创立了父类原型的正本,与父类原型齐全隔离*/Child.prototype = Object.create(Parent.prototype);Child.prototype.say = function() { console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);}// 留神记得把子类的结构指向子类自身Child.prototype.constructor = Child;var parent = new Parent('father');parent.say() // father: 你打篮球的样子像kunkunvar child = new Child('cxk', 'father');child.say() // father好,我是练习时长两年半的cxk
查找字符串中呈现最多的字符和个数
例: abbcccddddd -> 字符最多的是d,呈现了5次
let str = "abcabcabcbbccccc";let num = 0;let char = ''; // 使其依照肯定的秩序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},呈现了${num}次`);
深克隆(deepclone)
简略版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无奈实现对函数 、RegExp等非凡对象的克隆
- 会摈弃对象的constructor,所有的构造函数会指向Object
- 对象有循环援用,会报错
面试版:
/** * deep clone * @param {[type]} parent object 须要进行克隆的对象 * @return {[type]} 深克隆后的对象 */const clone = parent => { // 判断类型 const isType = (obj, type) => { if (typeof obj !== "object") return false; const typeString = Object.prototype.toString.call(obj); let flag; switch (type) { case "Array": flag = typeString === "[object Array]"; break; case "Date": flag = typeString === "[object Date]"; break; case "RegExp": flag = typeString === "[object RegExp]"; break; default: flag = false; } return flag; }; // 解决正则 const getRegExp = re => { var flags = ""; if (re.global) flags += "g"; if (re.ignoreCase) flags += "i"; if (re.multiline) flags += "m"; return flags; }; // 保护两个贮存循环援用的数组 const parents = []; const children = []; const _clone = parent => { if (parent === null) return null; if (typeof parent !== "object") return parent; let child, proto; if (isType(parent, "Array")) { // 对数组做非凡解决 child = []; } else if (isType(parent, "RegExp")) { // 对正则对象做非凡解决 child = new RegExp(parent.source, getRegExp(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (isType(parent, "Date")) { // 对Date对象做非凡解决 child = new Date(parent.getTime()); } else { // 解决对象原型 proto = Object.getPrototypeOf(parent); // 利用Object.create切断原型链 child = Object.create(proto); } // 解决循环援用 const index = parents.indexOf(parent); if (index != -1) { // 如果父数组存在本对象,阐明之前曾经被援用过,间接返回此对象 return children[index]; } parents.push(parent); children.push(child); for (let i in parent) { // 递归 child[i] = _clone(parent[i]); } return child; }; return _clone(parent);};
局限性:
- 一些非凡状况没有解决: 例如Buffer对象、Promise、Set、Map
- 另外对于确保没有循环援用的对象,咱们能够省去对循环援用的非凡解决,因为这很耗费工夫
原理详解实现深克隆