前端面试---手写代码常考题

实现一个 new 操作符

  • 创建一个新的空对象
  • 使空对象的__proto__指向构造函数的原型(prototype)
  • 把this绑定到空对象
  • 执行构造函数,为空对象添加属性
  • 判断函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
  • --如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用。
function myNew(Con, ...args){
    let obj = {}
    obj.__proto__ = Con.prototype
    let result = Con.call(obj, ...args)
    return result instanceof Object ? result : obj
}
let lin = myNew(Star,'lin',18)
// 相当于
let lin = new Star('lin',18)

实现 call

call核心:

  • 第一个参数为null或者undefined时,默认上下文为全局对象window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
  • 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型
  • 调用函数
  • 函数执行完成后删除 context.fn 属性
  • 返回执行结果
Function.prototype.myCall = function (context=window, ...args) {
  let fn = Symbol('fn');
  context.fn = this;  // 这里的this就是需要调用的函数,例子中的 bar(name, age) {}
  // 调用函数
  let result = context.fn(...args);  // 这里用了 扩展运算符,将 数组 转换成 序列

  delete context.fn
  return result;
}

// 测试
let foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
bar.myCall(foo, 'black', '18') // black 18 1

实现 apply

  • 前部分与call一样
  • 第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function (context=window, ...args) {
  let fn = Symbol('fn');
  context.fn = this; 
  let result = context.fn(args);

  delete context.fn
  return result;
}
  • 注:代码实现存在缺陷,当第二个参数为类数组时,未作判断(有兴趣可查阅一下如何判断类数组)

实现 bind

bind()方法:会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

  • 对于普通函数,绑定this指向
  • 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind = function(context, ...args) {
	const self = this;
	let bindFn = function() {
		self.apply(
			// 对于普通函数,绑定this指向
			// 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象 
			this instanceof bindFn ? this : context,
			args.concat(...arguments));  // ...arguments这里是将类数组转换为数组
	}
	bindFn.prototype = Object.create(self.prototype);
	// 返回一个函数
	return bindFn;
}

以下是对实现的分析:

let foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
bar.myBind(foo, 'black', '18') 
  • 前几步和之前的实现差不多,就不赘述了
  • bind 返回了一个函数,
  • 对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,
  • 我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,
  • 但是对于参数需要注意以下情况:
  • 因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),
  • 所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)

【补充】
JavaScript 中 call()、apply()、bind() 的用法

bind VS call/apply

  1. 一个函数被 call/apply 的时候,会直接调用
  2. bind 会创建一个新函数

当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

context = this instanceof Fn ? this : context; // 判断执行上下文

实现instanceof

object instanceof constructor

  • object某个实例对象, constructor某个构造函数
  • instanceof 运算符用来检测 某个构造函数.prototype 是否存在于参数 某个实例对象 的原型链上。
    instanceof
function myInstanceof(left, right) {
	//基本数据类型直接返回false
	if (typeof left !== 'object' || left == null) return false;
	//getPrototypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
	let proto = Object.getPrototypeOf(left);
	while (true) {
		// 如果查找到尽头,还没找到,return false
		if (proto == null) return false;
		//找到相同的原型对象
		if (proto === right.prototype) return true;
		proto = Object.getPrototypeOf(proto);
	}
}

console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String)); //true

浅拷贝

1. 循环遍历

let obj2 = {};
// 循环遍历
for (let k in obj1) {
	// k 是属性名  obj1[k] 属性值
	obj2[k] = obj1[k];
}

2. ES6语法 Object.assign

let obj2 = {}; 
Object.assign(obj2,obj1);

//补充
Object.assign() // 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources)

3. ES6语法 扩展运算符

let obj2 = {...obj1};

4. arr.slice()

// 语法: arr.slice(begin, end)
let arr1 = [1, 2, 3, 4]
let arr2 = arr1.slice();

深拷贝

1. 简易版及问题

JSON.parse(JSON.stringify(obj));

该方法的局限性:

  • 无法解决循环引用的问题。举个例子:
    const a = {val:2};a.target = a;
    拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  • 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map等

  • 无法拷贝函数

  • 会忽略 undefined/symbol

2. 面试可用版

function deepClone(obj){
	if(typeof obj == 'object'){
		//初始化返回结果
		let result = Array.isArray(obj) ? [] : {};
		for (let key in obj) {
			// 保证 key 不是原型上的属性
			if (obj.hasOwnProperty(key)) {
				// 递归调用
				result[key] = deepClone(obj[key]);
			}
		}
	}else{
		//简单数据类型 直接 赋值
		let result = obj;
	}
	return result;
}

防抖

所谓防抖,就是指触发事件后在设定的时间期限内函数只能执行一次,如果在设定的时间期限内又触发了事件,则会重新计算函数执行时间。

function debounce(fn, delay) {
	let timer = null;
	return function(...args) {
		let context = this;
		if (timer) clearTimeout(timer);
		timer = setTimeout(function() {
			fn.apply(context, args);
			timer = null;
		}, delay);
	}
}

节流-定时器

所谓节流,就是指连续触发事件但是在设定时间内中只执行一次函数。

function throttle(fn, delay = 100) {
	let timer = null;
	return function(...args) {
		if (!timer) {
			timer = setTimeout(() => {
				fn.apply(this, args)
				timer = null;
			}, delay)
		}
	}
}

节流-时间戳

function throttle(fn, delay = 100) {
    let previous = 0
    return function(...args) {
		let context = this
        let now = +new Date()
        if(now - previous > delay) {
            previous = now
            fn.apply(context, args)
        }
    }
}

双剑合璧

懒加载

// 获取所有的图片标签
const imgs = document.getElementsByTagName('img')
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0,
	len = imgs.length

function lazyload() {
	for (let i = num; i < len; i++) {
		// 用可视区域高度 减去 元素顶部距离可视区域顶部的高度
		let distance = viewHeight - imgs[i].getBoundingClientRect().top
		// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
		// 在chrome浏览器中 正常
		if (distance >= 50) {
			// 给元素写入真实的 src,展示图片
			imgs[i].src = imgs[i].getAttribute('data-src')
			// 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
			num = i + 1
		}
	}
}

// 监听scroll事件
window.addEventListener('scroll', lazyload)

当然,最好对 scroll 事件做节流处理,以免频繁触发:
window.addEventListener('scroll', throttle(lazyload, 200));

参考

「中高级前端面试」JavaScript手写代码无敌秘籍

初、中级前端应该要掌握的手写代码实现

原生JS灵魂之问(中)

posted @ 2020-04-04 00:43  Chrislinlin  阅读(1711)  评论(0编辑  收藏  举报