前端开发面试宝典-Javascript部分

165 阅读4分钟

Javascript部分

手写promise

function MyPromise(executor) {
    var self = this;
    self.status = 'pending';
    self.value = undefined;
    self.reason = undefined;
    self.onResolvedCallbacks = [];
    self.onRejectedCallbacks = [];
    function resolve(value) {
        if(self.status === 'pending') {
            self.status = 'fulfilled';
            self.value = value;
            // 执行所有成功的回调函数
            self.onResolvedCallbacks.forEach(function(callback){
                callback(self.value)
            })
            
        }
    }
    function reject(reason) {
        if(self.status === 'pending') {
            self.status = 'rejected';
            self.reason = reason;
            // 执行所有失败的回调函数
            self.onRejectedCallbacks.forEach(function(callback){
                callback(self.reason)
            })
        }
    }
    try {
        executor(resolve, reject);// 执行传入的executor函数
    } catch(e) {
        reject(e);// 执行过程中发生错误,则将Promise状态变为rejected
    }
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    var self = this;
    // 处理onFulfilled和onRejected不是函数的情况
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled:function(value) { return value;};
    onRejected = typeof onRejected === 'function' ? onRejected:function(reason) { return reason;};
    if(self.status === 'fulfilled') {
        // 执行成功的回调函数
        onFulfilled(self.value);
    }else if(self.status === 'rejected') {
        // 执行失败的回调函数
        onRejected(self.reason);
    }else if(self.status === 'pending') {
        // 将成功和失败的回调函数存储起来
        self.onResolvedCallbacks.push(function() {
            onFulfilled(self.value);
        })
        self.onRejectedCallbacks.push(function() {
            onRejected(self.reason);
        })
    }
}
// 测试
var promise = new MyPromise(function(resolve, reject) {
    setTimeout(function() {
        resolve('Hello, Promise!');
    }, 1000);
})
promise.then(function(value) {
    console.log('Resolved:' + value);
}, function(reason) {
    console.log('Rejected:' + reason);
})

手写bind

    Function.prototype.myBind = function(context, ...args) {
        var fn = this;
        return function(...innerArgs) {
            return fn.apply(context, args.concat(innerArgs))
        }
    }
    var obj = {
        name: 'John'
    }
    function getgreeting(msg) {
        console.log(`${msg}, ${this.name}`)
    }
    var broundGetgreeting = getgreeting.myBind(obj, 'Hello') 
    broundGetgreeting();// 打印结果 Hello,John

手写ajax、使用Promise封装ajax

//手写ajax
let xhr = new XMLHttpRequest();
xhr.open("GET", 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");
xhr.send(null);
//promise 封装实现:
function getJson(url){
    //创建一个promise对象
    let promise = new Promise(function(resolve,reject){
        let xhr = new XMLHttpRequest();
        //新建一个http请求
        xhr.open("GET",url,true);
        //设置状态的监听函数
        xhr.onreadystatechange = function(){
            if(this.readyState != 4){
                return;
            }
            //当请求成功或失败时,改变promise状态
            if(this.status === 200){
                   resolve(this.response);
            }else{
                reject(new Error(this.statusText));
            }
        };
    
        //设置错误监听函数
        xhr.onerror = function(){
            reject(new Error(this.statusText));
 
        };
        //设置响应的数据类型
        xhr.repoonseType = "json";
        //设置请求头信息
        xhr.setRequestHeader("Accept","application/json");
        //发送http
        xhr.send(null);
    })
    return promise;
}

bind、call、apply的区别

主要区别在于传递参数的方式和调用时机:

  • bind() 返回一个绑定了指定上下文的新函数,并可以在稍后调用。
  • call() 立即调用函数,参数通过逗号分隔传递给函数。
  • apply() 立即调用函数,参数以数组形式传递给函数。

示例 1: 使用 bind()

function greet(param1, param2) {
  console.log(`${param1}, ${this.firstName} ${this.lastName}${param2}`);
}
const person = {
  firstName: 'John',
  lastName: 'Doe',
};
// 使用 bind() 方法绑定函数,并传递额外的参数
const greetJohn = greet.bind(person, 'Hello');
// 调用绑定后的函数,传递剩余的参数
greetJohn('!'); // 输出:Hello, John Doe!

示例 2: 使用 call()

function greet(message) {
  console.log(`${message}, ${this.firstName} ${this.lastName}!`);
}
const person1 = {
  firstName: 'John',
  lastName: 'Doe',
};
const person2 = {
  firstName: 'Jane',
  lastName: 'Smith',
};
// 使用 call() 方法调用函数,并传递不定数量的参数
greet.call(person1, 'Hello'); // 输出:Hello, John Doe!
greet.call(person2, 'Hi');    // 输出:Hi, Jane Smith!

示例 3: 使用 apply()

function greet(message, punctuation) {
  console.log(`${message}, ${this.firstName} ${this.lastName}${punctuation}`);
}
const person = {
  firstName: 'John',
  lastName: 'Doe',
};
const messageArgs = ['Hello', '!'];
// 使用 apply() 方法调用函数,并传递参数数组
greet.apply(person, messageArgs); // 输出:Hello, John Doe!

cookie和localStorage和sessionStorage的区别

  • cookie在服务器和浏览器之间来回传递
  • localStorage独立存储空间5M,存储空间较大,cookie(4kb)
  • sessionStorage存储关闭浏览器或者标签页消失
  • localStorage同源限制,不同标签页可以共享数据。 sessionStorage即使同源,其他标签页也不能共享数据

cookie

  • 读取cookie document.cookie
  • 修改cookie document.cookie="userId=828; userName=hulk";
  • 删除cookie:
//获取当前时间
var date=new Date();
//将date设置为过去的时间
date.setTime(date.getTime()-1);
//将userId这个cookie删除
document.cookie="userId=828; expire="+date.toGMTString();
//设置cookie有效时间
function setCookie(c_name, value, expiredays){
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString());
}
  使用方法:setCookie('username','Darren',30)
//如果想以其他单位(如:小时)来设置,那么改变第三行代码即可:exdate.setHours(exdate.getHours() + expiredays);

原型链和作用域链 以及上下文、闭包

原型继承和class继承

涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?

首先先来讲下 class,其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。

class Person {} 
Person instanceof Function // true
组合继承

最常用的继承方式

优点:构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数。

缺点:就是在继承父类函数的时候调用了父类构造函数导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

function Parent(value) {
    this.val = value;
}
Parent.prototype.getValue = function() {
    console.log(this.val);
}
function Child(value) {
    Parent.call(this, value);
}
Child.prototype = new Parent();
const child = new Child(1);
child.getValue(); // 1 
child instanceof Parent; // true
寄生组合继承

对组合继承进行了优化

function Parent(value) { 
    this.val = value 
} 
Parent.prototype.getValue = function() { 
    console.log(this.val) 
} 
function Child(value) { 
    Parent.call(this, value) 
} 
Child.prototype = Object.create(Parent.prototype, { 
    constructor: { 
        value: Child, 
        enumerable: false, 
        writable: true, 
        configurable: true 
    } 
}) 
const child = new Child(1) 
child.getValue() // 1 
child instanceof Parent // true
class继承
class Parent {
    constructor(value) {
        this.val = value;
    }
    getValue() {
        console.log(this.val);
    }
}
class Child extends Parent {
    constructor(value) {
        super(value); // 等同于Parent.call(this, value)
        this.val = value;
    }
}
const child = new Child(1) 
child.getValue() // 1 
child instanceof Parent // true

原型链

  • 查找对象的某个属性时,先在当前对象上面查找,如果没有就去对象的__proto__中去查找,一直到查到Object的__proto__最顶层null,这也形成了一条查找链就是原型链

作用域链

  • 为了访问变量而形成的链
  • 全局作用域、函数作用域(块级作用域)、eval作用域
  • 多个作用域相互嵌套,形成作用域链。分级向上查找,全局作用域查找不到就停止搜索

闭包

  • 概念:函数嵌套,函数在其作用域以外的地方被调用就形成了闭包 词法作用域是根据代码的位置来决定的
  • 闭包解决的问题:1.可以读取函数内部的变量 2.让这些变量的值始终保持在内存中,不会在函数调用后被清除
  • 闭包的缺点:滥用闭包造成内存消耗过大,降低网页性能
  • 应用场景
for(var i = 0; i < 10; i++) {
 setTimeout(function() {
   console.log(i);//输出10个10
 }, 1000)
}
//闭包的应用
for(var i = 0; i < 10; i++) {
 ((j)=> {
   setTimeout(function() {
     console.log(j);//输出0-9
   }, 1000)  
 })(i)
}

浅拷贝和深拷贝

浅拷贝的方法
  • 直接赋值
let obj = {
    name: 'iyongbao',
    age: 26
}

let obj2 = obj;
obj2.name = "zhangsan";

console.log(obj); // {name: "zhangsan", age: 26}
console.log(obj2); // {name: "zhangsan", age: 26}
  • Object.assign

注意:使用Object.assign第一层是深拷贝。

let obj = {
    name: 'iyongbao',
    score: {
        vue: 98
    }
}

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

obj2.name = "zhangsan";
obj2.score.vue = 60;

console.log(obj); // {name: "iyongbao", score: { vue: 60 }}
console.log(obj2); // {name: "zhangsan", score: { vue: 60 }}
  • 扩展运算符

注意:同 使用Object.assign第一层是深拷贝。

let obj = {
    name: 'iyongbao',
    score: {
        vue: 98
    }
}

let obj2 = { ...obj };

obj2.name = "zhangsan";
obj2.score.vue = 60;

console.log(obj); // {name: "iyongbao", score: { vue: 60 }}
console.log(obj2); // {name: "zhangsan", score: { vue: 60 }}
  • slice和concat
let obj = ['iyongbao', { vue: 98 }]

let obj2 = obj.slice();
let obj3 = obj.concat();

obj[0] = "zhangsan";
obj[1].vue = 60;

console.log(obj); // {name: "zhangsan", score: { vue: 60 }}
console.log(obj2); // {name: "iyongbao", score: { vue: 60 }}
console.log(obj3); // {name: "iyongbao", score: { vue: 60 }}
深拷贝的方法
  • JSON.parse(JSON.stringify(待拷贝对象))

注意:JSON.stringify还是存在一些不足的,比如对(函数、undefined、正则、Symbol)不友好

let obj = {
    name: 'iyongbao',
    age: 26
}

let obj2 = JSON.parse(JSON.stringify(obj));
obj2.name = "zhangsan";

console.log(obj); // {name: "iyongbao", age: 26}
console.log(obj2); // {name: "zhangsan", age: 26}
  • 使用第三方库 Lodash
const _ = require('lodash');

let obj = {
    name: 'iyongbao',
    age: 26
}

let obj2 = _.cloneDeep(obj);
obj2.name = "zhangsan";

console.log(obj); // {name: "iyongbao", age: 26}
console.log(obj2); // {name: "zhangsan", age: 26}
  • 手写一个深拷贝
function deepClone (obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    }
    
    let deepCloneObj = Array.isArray(obj) ? [] : {}
    
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            deepCloneObj[key] = deepClone(obj[key]);
        }
    }
    
    return deepCloneObj;
}

es6新增特性

  • let const Map Set WeakMap WeakSet Generator async await Proxy Reflect Promise module

为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性
AMD/CMD
// AMD 
define(['./a', './b'], 
function(a, b) { 
    // 加载模块完毕可以使用 
    a.do() 
    b.do() 
}) 
// CMD 
define(function(require, exports, module) { 
    // 加载模块 
    // 可以把 require 写在函数体的任意地方实现延迟加载 
    var a = require('./a') 
    a.doSomething() 
})
CommonJS
// a.js 
module.exports = { a: 1 } // or 
exports.a = 1 // b.js 
var module = require('./a.js') 
module.a // -> log 1
ES Module
// 引入模块 API 
import XXX from './a.js' 
import { XXX } from './a.js' 
// 导出模块 API 
export function a() {} 
export default function() {}

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的

setTimeout、setInterval、requestAnimationFrame 各有什么特点?

备注:JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout setInterval 不会按期执行。 requestAnimationFrame自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout

所以,定时器可通过 requestAnimationFrame 方案代替

// 模拟setInterval
function setInterval(callback, interval) {
	let timer
	const now = Date.now
	let startTime = now()
	let endTime = startTime
	const loop = () => {
		timer = window.requestAnimationFrame(loop)
		endTime = now()
		if (endTime - startTime >= interval) {
			startTime = endTime = now() callback(timer)
		}
	}
	timer = window.requestAnimationFrame(loop)
	return timer
}
let a = 0
setInterval(timer => {
	console.log(1) a++
	if (a === 3) cancelAnimationFrame(timer)
}, 1000)

// 模拟setTimeout
function setTimeoutPolyfill(callback, delay) {
	let start = null;

	function animate(timestamp) {
		if (!start) start = timestamp;
		const elapsed = timestamp - start;

		if (elapsed >= delay) {
			callback();
		} else {
			requestAnimationFrame(animate);
		}
	}

	requestAnimationFrame(animate);
}

// 示例
setTimeoutPolyfill(() => {
	console.log('setTimeoutPolyfill executed after 1000ms');
}, 1000);

算法

  • N数之和
function addNum() {
 var num = 0;
 for(var i = 0; i < arguments.length; i++) {
   num += arguments[i];
 }
 return num;
}
addNum(1, 5, 6);
  • 双指针
/*
给你一个升序排列的数组nums ,请你原地删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度。
元素的相对顺序应该保持一致 。
当nums长度为0时,数组无数据,返回值为0;
当nums长度为1时,数组只有一个元素,所以无重复值无需处理,返回值为1;
当nums长度大于1时,设数组的长度为length,快指针为fast=1,慢指针slow=1,遍历数组,当快指针小与数组长度时,判断两个相邻元素不相同,nums[fast] 不等于 nums[fase -1],不相同,则,将快指针的元素赋值给慢指针nums[slow] = nums[fast],同时慢指针slow加1;遍历结束,则慢指针slow即为去重后的数组长度
*/

 function uniq(nums) {
   let length = nums.length;
   if (length === 0) {
     return 0;
   }
   let fast = 1,
     slow = 1;
   while (fast < length) {
     if (nums[fast] !== nums[fast - 1]) {
       nums[slow] = nums[fast];
       ++slow;
     }
     ++fast;
   }
   return slow;
 }
 uniq([1, 1, 2]);
  • 动态规划DP算法
  • 快慢指针