前端手写代码原理

378 阅读6分钟

1.用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。如:输入整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量,输入函数必须只有一个参数传入,必须返回字符串。

function fun(num){
    let num1 = num / 10;
    let num2 = num % 10;
    if(num1<1){
        return num;
    }else{
        num1 = Math.floor(num1)
        return `${num2}${fun(num1)}`
    }
}
var a = fun(12345)
console.log(a)

2.请实现一个 add 函数,满足以下功能

add(1); 			// 1
add(1)(2);  	// 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

答案:

function currying(fn, length) {
  length = length || fn.length; 	// 第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
  return function (...args) {			// currying 包裹之后返回一个新函数,接收参数为 ...args
    return args.length >= length	// 新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
    	? fn.apply(this, args)			// 满足要求,执行 fn 函数,传入新函数的参数
      : currying(fn.bind(this, ...args), length - args.length) // 不满足要求,递归 currying 函数,新的 fn 为 bind 返回的新函数(bind 绑定了 ...args 参数,未执行),新的 length 为 fn 剩余参数的长度
  }
}

const add = currying(function(a, b, c) {
    console.log(a+b+c);
});
add(1)(2,3)

它的ES6极简写法:

const currying = fn =>
    judge = (...args) =>
        args.length >= fn.length
            ? fn(...args)
            : (...arg) => judge(...args, ...arg)

引申:该题用了函数柯里化

  • 定义:柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术
  • 实际应用
    • 延迟计算:部分求和、bind 函数
    Function.prototype.bind = function (context) {
    var self = this;
    // 第 1 个参数是指定的 this,截取保存第 1 个之后的参数
    	// arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 
    
    return function () {
        // 此时的 arguments 是指 bind 返回的函数调用时接收的参数
        // 即 return function 的参数,和上面那个不同
      	// 类数组转成数组
        var bindArgs = Array.prototype.slice.call(arguments);
      	// 执行函数
        return self.apply( context, args.concat(bindArgs));
    }
    }
    
    • 动态创建函数:添加监听 addEvent、惰性函数
    • 参数复用:
     const toStr = Function.prototype.call.bind(Object.prototype.toString)
     toStr([1, 2, 3]); 	// "[object Array]"
     toStr('123'); 		// "[object String]"
     toStr(123); 		// "[object Number]"
     toStr(Object(123)); // "[object Number]"
    
  • 实现 currying 函数:用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
  • 函数参数length:获取的是形参的个数 ,但是形参的数量不包括剩余参数个数,而且仅包括第一个具有默认值之前的参数个数

参考链接:深入高阶函数应用之柯里化

3.打印出 1 - 10000 之间的所有对称数

[...Array(10000).keys()].filter((x) => { 
  return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join('')) 
})

4.为什么普通for循环的性能远远高于 forEach 的性能,请解释其中的原因

  • for 循环没有任何额外的函数调用栈和上下文;
  • forEach函数实际上是 array.forEach(function(currentValue, index, arr), thisArg). 它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能

5.ES6 代码转成 ES5 代码的实现思路是什么

Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:

  • ES6的代码字符串解析成抽象语法树,即所谓的 AST
  • AST 进行处理,将ES6 AST转为ES5 AST
  • 根据处理后的 AST 再生成代码字符串 基于此,其实我们自己就可以实现一个简单的“编译器”,用于把 ES6代码转成 ES5

比如,可以使用 @babel/parserparse 方法,将代码字符串解析成 AST;使用 @babel/coretransformFromAstSync 方法,对 AST 进行处理,将其转成 ES5 并生成相应的代码字符串;过程中,可能还需要使用 @babel/traverse 来获取依赖文件等。

6.箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  3. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  4. 不可以使用 new 命令,因为:
    • 没有自己的 this,无法调用 callapply
    • 没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 __proto__

7.实现 (5).add(3).minus(2) 功能

考虑到了浮点型的运算

Number.MAX_SAFE_INTEGER 常量表示在 JavaScript 中最大的安全整数(maxinum safe integer)(2^53 - 1)。

Number.MIN_SAFE_INTEGER 代表在 JavaScript中最小的安全的integer型数字 (-(2^53 - 1)).

Number.MAX_SAFE_DIGITS = Number.MAX_SAFE_INTEGER.toString().length-2
Number.prototype.digits = function(){
	let result = (this.valueOf().toString().split('.')[1] || '').length
	return result > Number.MAX_SAFE_DIGITS ? Number.MAX_SAFE_DIGITS : result
}
Number.prototype.add = function(i=0){
	if (typeof i !== 'number') {
        	throw new Error('请输入正确的数字');
    	}
	const v = this.valueOf();
	const thisDigits = this.digits();//当前的小数位数
	const iDigits = i.digits();//要相加的数的小数位数
	const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));//变成整数
	const result = (v * baseNum + i * baseNum) / baseNum;//相加后再变小数
	if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
	else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
Number.prototype.minus = function(i=0){
	if (typeof i !== 'number') {
        	throw new Error('请输入正确的数字');
    	}
	const v = this.valueOf();
	const thisDigits = this.digits();
	const iDigits = i.digits();
	const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
	const result = (v * baseNum - i * baseNum) / baseNum;
	if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
	else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}

8.为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
  • 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
  • 跨域友好
  • 执行过程无阻塞
  • 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
  • GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)

9.使用迭代的方式实现 flatten 函数

迭代的实现

let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]]

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

递归的实现(ES6简写):

const flatten = array => array.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flatten(cur)] : [...acc, cur]), [])

10.实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现

//Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
  console.log(1)
})

//Generator
function* sleepGenerator(time) {
  yield new Promise(function(resolve,reject){
    setTimeout(resolve,time);
  })
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})

//async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

//ES5
function sleep(callback,time) {
  if(typeof callback === 'function')
    setTimeout(callback,time)
}

function output(){
  console.log(1);
}
sleep(output,1000);

11.[]==![]返回true

Boolean([])  //true
  • 任意值与布尔值比较,都会将两边的值转化为Number![]false
  • arrfalse比较,false转化为0,而arr为空数组,也转化为0
  • 所以,当空数组作为判断条件时,相当于true。当空数组与布尔值直接比较时,相当于false

12.100*100的 canvas 占多少内存?

  • 如果了解过 Canvas 且做过滤镜相关的工作,可能调用过 imageData = ctx.getImageData(sx, sy, sw, sh); 这个 API
  • 这个 API 返回的是一个ImageData数组,包含了 sx, sy, sw, sh 表示的矩形的像素数据。而且这个数组是 Uint8 类型的,且四位表示一个像素。(Uint8Array 数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素)
  • 猜想一下,我们在定义颜色的时候就是使用 rgba(r,g,b,a) 四个维度来表示,而且每个像素值就是用十六位 00-ff 表示,即每个维度的范围是 0~255,即 2^8 位,即 1 byte, 也就是 Uint8 能表示的范围。 所以 100 * 100 canvas 占的内存是 100 * 100 * 4 bytes = 40,000 bytes

参考链接:100*100的 canvas 占多少内存?

13.模版字符串匹配

//自定义全局替换函数
/**g,表示全文匹配;
  *m,表示多行匹配(也就是正则表达式出现“^”、“$”,如果要匹配的字符串其中有换行符也没关系);
  *i,表示忽略大小写
  */
  //默认地,replace() 只替换首个匹配:如需替换所有匹配,请使用正则表达式的 g 标志(用于全局搜索),str.replace(/Microsoft/g, "W3School"),请注意正则表达式不带引号
function template(tmpl, data) {
   let keys= Object.keys(data)
    for(let i=0;i<keys.length;i++){
            tmpl= tmpl.replace(new RegExp("{{" + keys[i] + "}}",'gm'),data[keys[i]])
    }
    return tmpl
  }
  template('我的名字是{{name}},我的工作是{{work}},我喜欢{{work}}', {
    name: '小周',
    work: '编程'
  });
  // 函数的返回是 '我的名字是小周,我的工作是编程,我喜欢编程'

14.反转DOM元素子节点顺序

function reverseChildNodes(node) {
    //先将要倒序的DOM元素的父节点以及紧邻的同级及节点储存下来
    var parentNode = node.parentNode, nextSibling = node.nextSibling,
        frag = node.ownerDocument.createDocumentFragment();
    //然后将DOM元素从其父节点移除
    parentNode.removeChild(node);
    //倒序插入
    while(node.lastChild)
        frag.appendChild(node.lastChild);
    //再把储存了倒序节点的DocumentFragment放入原DOM元素中,再用inserBefore()方法插入原位置
    node.appendChild(frag);
    parentNode.insertBefore(node, nextSibling);
    return node;
}
reverseChildNodes(document.getElementById('con'));

或者

//appendChild()`方法不只是可以向document的某节点插入一个手动创建的节点,还可以将document中已经存在的节点移动到所需的插入位置
function reverseChildNodes(node) {
    var frag = node.ownerDocument.createDocumentFragment();
    while(node.lastChild)
        frag.appendChild(node.lastChild);
    node.appendChild(frag);
}
reverseChildNodes(document.getElementById('con'));

15.实现一个图片懒加载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>图片懒加载</title>
    <style>
        img {
            display: block;
            width: 100%;
            height: 300px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <img data-src="./images/1.jpg" alt="">
    <img data-src="./images/2.jpg" alt="">
    <img data-src="./images/3.jpg" alt="">
    <img data-src="./images/4.jpg" alt="">
    <img data-src="./images/5.jpg" alt="">
    <img data-src="./images/6.jpg" alt="">
    <img data-src="./images/7.jpg" alt="">
    <img data-src="./images/8.jpg" alt="">
    <img data-src="./images/9.jpg" alt="">
    <img data-src="./images/10.jpg" alt="">
    <img data-src="./images/1.jpg" alt="">
    <img data-src="./images/2.jpg" alt="">
</body>
<script>
        var imgs = document.querySelectorAll('img');

        //offsetTop是元素与offsetParent的距离,循环获取直到页面顶部offsetParent属性返回一个对象
        //的引用,这个对象是距离调用offsetParent的元素最近的(在包含层次中最靠近的),并且是已进行过
        //CSS定位的容器元素。 如果这个容器元素未进行CSS定位, 则offsetParent属性的取值为
        //根元素(在标准兼容模式下为html元素;在怪异呈现模式下为body元素)的引用。
        //当容器元素的style.display 被设置为 "none"时(译注:IE和Opera除外),
        //offsetParent属性 返回 null。

        function getTop(e) {
            var T = e.offsetTop;
            while(e = e.offsetParent) {
                T += e.offsetTop;
            }
            return T;
        }

        function lazyLoad(imgs) {
            var H = document.documentElement.clientHeight;//获取可视区域高度
            var S = document.documentElement.scrollTop || document.body.scrollTop;
            for (var i = 0; i < imgs.length; i++) {
                if (H + S > getTop(imgs[i])) {
                    imgs[i].src = imgs[i].getAttribute('data-src');
                }
            }
        }

        window.onload = window.onscroll = function () { //onscroll()在滚动条滚动的时候触发
            lazyLoad(imgs);
        }
</script>
</html>

或者使用getBoundingClientRect()位置API

var imgs = document.querySelectorAll('img');

        //用来判断bound.top<=clientHeight的函数,返回一个bool值
        function isIn(el) {
            var bound = el.getBoundingClientRect();
            var clientHeight = window.innerHeight;
            return bound.top <= clientHeight;
        } 
        //检查图片是否在可视区内,如果在,则加载
        function check() {
            Array.from(imgs).forEach(function(el){
                if(isIn(el)){
                    loadImg(el);
                }
            })
        }
        function loadImg(el) {
            if(!el.src){
                var source = el.dataset.src;
                el.src = source;
            }
        }
        window.onload = window.onscroll = function () { //onscroll()在滚动条滚动的时候触发
            check();
        }

参考链接:原生js实现图片懒加载

16.实现一个基本的 Event Bus

// 组件通信,一个触发与监听的过程
class EventEmitter {
  constructor () {
    // 存储事件
    this.events = this.events || new Map()
  }
  // 监听事件
  addListener (type, fn) {
    if (!this.events.get(type)) {
      this.events.set(type, fn)
    }
  }
  // 触发事件
  emit (type) {
    let handle = this.events.get(type)
    handle.apply(this, [...arguments].slice(1))
  }
}

// 测试
let emitter = new EventEmitter()
// 监听事件
emitter.addListener('ages', age => {
  console.log(age)
})
// 触发事件
emitter.emit('ages', 18)  // 18

参考链接:JS高频手写代码题

引申:Vue.js事件总线(EventBus)

17.Object.create 的基本实现原理

// 思路:将传入的对象作为原型
function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

18.手写实现 AJAX

// 1. 简单流程

// 实例化
let xhr = new XMLHttpRequest()
// 初始化
xhr.open(method, url, async)
// 发送请求
xhr.send(data)
// 设置状态变化回调处理请求结果
xhr.onreadystatechange = () => {
  if (xhr.readyStatus === 4 && xhr.status === 200) {
    console.log(xhr.responseText)
  }
}

// 2. 基于promise实现

function ajax (options) {
  // 请求地址
  const url = options.url
  // 请求方法
  const method = options.method.toLocaleLowerCase() || 'get'
  // 默认为异步true
  const async = options.async
  // 请求参数
  const data = options.data
  // 实例化
  const xhr = new XMLHttpRequest()
  // 请求超时
  if (options.timeout && options.timeout > 0) {
    xhr.timeout = options.timeout
  }
  // 返回一个Promise实例
  return new Promise ((resolve, reject) => {
    xhr.ontimeout = () => reject && reject('请求超时')
    // 监听状态变化回调
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        // 200-300 之间表示请求成功,304资源未变,取缓存
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
          resolve && resolve(xhr.responseText)
        } else {
          reject && reject()
        }
      }
    }
    // 错误回调
    xhr.onerror = err => reject && reject(err)
    let paramArr = []
    let encodeData
    // 处理请求参数
    if (data instanceof Object) {
      for (let key in data) {
        // 参数拼接需要通过 encodeURIComponent 进行编码
        paramArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
      }
      encodeData = paramArr.join('&')
    }
    // get请求拼接参数
    if (method === 'get') {
      // 检测url中是否已存在 ? 及其位置
      const index = url.indexOf('?')
      if (index === -1) url += '?'
      else if (index !== url.length -1) url += '&'
      // 拼接url
      url += encodeData
    }
    // 初始化
    xhr.open(method, url, async)
    // 发送请求
    if (method === 'get') xhr.send(null)
    else {
      // post 方式需要设置请求头
      xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8')
      xhr.send(encodeData)
    }
  })
}

19.简单手写观察者模式

一个典型的观察者模式应用场景是用户订阅某个主题

  • 多个用户(观察者,Observer)都可以订阅某个主题(Subject
  • 当主题内容更新时订阅该主题的用户都能收到通知
  • Subject 是构造函数,new Subject() 创建一个主体对象,该对象内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者 (addObserver),删除观察者(removeObserver),通知观察者更新(notify), 当 notify 时实际上调用全部观察者 observer 自身的 update 方法
class Subject {
  constructor() {
    this.observers = []
  }
  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    var index = this.observers.indexOf(observer)
    if(index > -1) {
      this.observers.splice(index, 1)
    }
  }
  notify() {
    this.observers.forEach(observer => {
      observer.update()
    })
  }
}

class Observer {
  constructor(name) {
    this.name = name
  }
  update() {
    console.log(this.name + ' update...')
  }
  subscribeTo(subject) {
    subject.addObserver(this)
    console.log('订阅成功!')
  }
}

let subject = new Subject()
let observer = new Observer('zhangsan')
observer.subscribeTo(subject)

subject.notify()

参考链接:手写一个 MVVM 框架

20.根据高度上拉加载

判断内容高度-父元素高度-滑动高度scrollTop是否大于某个值就开始滑动

所有的高度:高度区别

 // 获取ref的高度
    getEleHeight (ele) {
      if (!ele || ele.nodeType === 8) {
        return 0
      }
      return ele.clientHeight
    },
    touchMove () {
      if (!this.debounceFn) {
        this.debounceFn = this.createdebounceFn(this.moveList)
      }
      this.debounceFn()
    },
    moveList () {
      const listShowLayoutHeight = this.getEleHeight(this.$refs.listShowLayout && this.$refs.listShowLayout.$el)
      const listLayoutHeight = this.getEleHeight(this.$refs.searchlistLayout)
      const scrollTop = this.$refs.searchlistLayout && this.$refs.searchlistLayout.scrollTop
      const listToBottom = listShowLayoutHeight - listLayoutHeight - scrollTop
      if (listToBottom <= 0 && !this.isLoading && this.hasMorePage) {
        console.log('滑到底部,该翻页了')
      }
    },
    // 创建防抖函数
    createdebounceFn (fn) {
      return debounce(() => {
        fn()
      }, 100)
    }