JS经典面试问答题&&手写代码题

281 阅读12分钟

前情提要

在准备前端校招实习面试过程中,总结了一些常见JavaScript基础问题,针对问题搜集并归纳了回答。

JS 基础问答题

1. ECMAScript && JavaScript

ECMAScript实际上是一种脚本在语法和语义上的标准
JavaScript是由ECMAScript,DOM和BOM三者组成的

2. defer(推迟) && async(异步)

defer:浏览器会异步的下载该文件并且不会影响到后续DOM的渲染;
如果有多个设置了defer的script标签存在,则会按照顺序执行所有的script;
defer 属性在 HTML 解析期间异步下载文件,并且只在 HTML 解析完成后才执行它;

async:会使得script脚本异步的加载并在允许的情况下执行async的执行,并不会按着script在页面中的顺序来执行,而是谁先加载完谁执行。async 属性会在 HTML 解析期间异步下载文件,并在完成下载后立即暂停 HTML 解析器去执行 script 中的代码。在执行过程中浏览器处于阻塞状态,响应不了任何需求。

defer&&async

3. JS基本数据类型

undefined
null:为什么typeof(null)是object,因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。可以通过 Object.prototype.toString.call(xx)进行判断类型
布尔
字符串
数值
对象
Symbol(ES6新增)

4. undefine && null

undefined表示不存在这个值,是一个表示“无”的原始值或者说表示“缺少值”,就是此处应该有一个值,但是还没有定义,当尝试读取时会返回undefined;假如变量被声明了,但没有赋值,就等于undefined,转为数值时为NaN

null表示一个对象被定义了,值为“空值”,转为数值时为0;
在验证null时,要用===,因为==无法区分null和undefined

undefinednull与其他类型的值比较时,结果都为false,它们互相比较时结果为true,undefinednull与自身严格相等

    undefined === undefined // true
    null === null // true
    null == undefined // true
    null === undefined // false

5. typeof可以判断几种数据类型

6种:undefined Boolean number string object(注意null也是返回object) function

6. 检测是否是数组的方法

typeof 方法失效 返回Object

    arr.isArray();
    arr instanceof Array;
    arr.constructor == Array;
    toString.call(new Array); // [object Array]

7. 闭包

闭包就是能够读取其他函数内部变量的函数.在js中变量有全局变量和局部变量,从函数的内部可以访问函数外部的变量,而函数外部不能访问函数内部的变量,这时候就出现了闭包,闭包实现了从函数外部,访问函数内部的变量。一般会将一个函数作为返回值,此时在调用该函数的时候,就可以访问函数内部的变量了。

为什么能够访问到变量: 是因为闭包的作用域链包含了他自己的作用域和包含他的函数的作用域以及全局作用域,一般来说,当一个函数执行完以后他的作用域和变量就会被销毁,但创建了一个闭包以后,这个函数作用域就会在一直保存到这个闭包不存在为止,所以当闭包要访问一个引用的变量时,会沿着作用域链去寻找。

闭包的坑:1.引用的变量可能已经改变 2.this指向问题 3.内存泄漏问题

闭包经典变量i问题:1.改为let 2.使用闭包 3.使用JS中参数是按值传递的特性

    // 原始状态
    for (var i=0;i<10;i++) {
        setTimeout(() => {
            console.log(i) // 10,10,10...
        })
    }
    // 改为let
    for (let i=0;i<10;i++) {
        setTimeout(() => {
            console.log(i) // 0,1,2...,9
        })
    }
    // 使用闭包
    // 定时函数的变量作用域就变为匿名函数代码块内,每次for循环传给定时器的i值都会变为定时函数的私有变量值,这样就达到了预期目的
    for (var i=0;i<10;i++) {
        (function (num) {
            setTimeout(() => {
                console.log(num) // 0,1,2...,9
            })
        })(i)
    }
    // 使用JS中参数是按值传递的特性
    for (var i=0;i<10;i++) {
        setTimeout((j) => {
            console.log(j) // 0,1,2...,9
        },1000,i);
    }

8. 如何新建一个对象

  1. 原始方法
    var obj = new Object()
  1. 工厂方法
    function createObj(name,age){
        var obj = new Object();
        obj.name = name;
        obj.age = age;
        obj.showName = function () {
            console.log(this.name);
        };
        obj.showAge = function(){
            console.log(this.age);
        };
        return obj;
    }
  1. 构造函数方法
    function Person(name,age){
        this.name = name;
        this.age = age;

        this.showName = function () {
            console.log(this.name);
        };
        this.showAge = function () {
            console.log(this.age);
        };
    }
  1. 原型方法(属性会共享,当生成Person对象时,prototype的属性都赋给了新的对象。那么属性和方法是共享的。首先,该方法的问题是构造函数不能传递参数,每个新生成的对象都有默认值。其次,方法共享没有任何问题,但是,当属性是可改变状态的对象时,属性共享就有问题。)
    function Person(){} //定义一个空构造函数,且不能传递参数
    //将所有的属性的方法都赋予prototype
    Person.prototype.name = "Kitty";
    Person.prototype.age = 21;
    Person.prototype.showName = function (){
        console.log(this.name);
    };
    Person.prototype.showAge = function (){
        console.log(this.age);
    };

    var obj1 = new Person("Kitty","21");
    var obj2 = new Person("Luo","22");

    obj1.showName();//Kitty
    obj1.showAge();//21

    obj2.showName();//luo
    obj2.showAge();//22
  1. 混合的构造函数/原型方式:(属性私有,方法公有) 属性私有后,改变各自的属性不会影响别的对象。同时,方法也是由各个对象共享的。在语义上,这符合了面向对象编程的要求。
    function Person(name,age){
        this.name = name;
        this.age = age;
        this.array = new Array("Kitty","luo");
    }

    Person.prototype.showName = function (){
        console.log(this.name);
    };
    Person.prototype.showArray = function (){
        console.log(this.array);
    };
    var obj1 = new Person("Kitty",21);
    var obj2 = new Person("luo",22);
    obj1.array.push("Wendy");//向obj1的array属性添加一个元素

    obj1.showArray();//Kitty,luo,Wendy
    obj1.showName();//Kitty
    obj2.showArray();//Kitty,luo
    obj2.showName();//luo

9. 事件委托

事件冒泡:事件会从最内层的元素开始发生,一直向上传播,直到document对象

阻止事件冒泡的方法

  • event.stopPropagation() 事件处理过程中,阻止了事件冒泡,但不会阻止默认行为
  • return false 事件处理过程中,阻止了事件冒泡,也阻止了默认行为
  • event.preventDefault() 事件处理过程中,不阻止事件冒泡,但阻止默认行为

事件捕获:事件会从最外层开始发生,直到最具体的元素

事件委托一般是通过事件冒泡实现的,可以减少内存消耗(不必为每一个li绑定事件),动态绑定事件(后来添加的li也会有此事件,因为是绑定在父元素上的)

10. 数组遍历

  • for:可以break

  • foreach:会跳过空元素,不能break

  • for in:跳过空元素,会遍历非数字属性,遍历的是index

  • for of:遍历的是值,不能遍历出下标

11. 对象遍历

  • For-in: 遍历输出的是对象自身的属性以及原型链上可枚举的属性(不含Symbol属性)

  • Object.keys():方法会返回一个自身可枚举属性组成的数组

  • Object.values():方法会返回一个自身可枚举属性值组成的数组

  • Object.entries():方法会返回一个自身可枚举属性的键值对组成的数组

  • Object.getOwnPropertyNames():方法返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性

判断一个对象是否为空对象

    // 1.for-in 循环判断
    var obj = {}
    function judgeObj(obj) {
        for (let key in obj) {
            return false;
        }
        return true;
    }
    // 2.Object.getOwnPropertyNames()
    var obj = {}
    Object.getOwnPropertyNames(obj).length === 0; // true
    // 3.Object.keys()
    var obj = {}
    Object.keys(obj).length === 0; // true

12. 作用域

作用域:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

作用域链:保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链的本质是一个指向变量对象的指针列表。

在JS中一共有三种作用域:

  • 全局作用域
  • 函数作用域
  • 块级作用域

注意!块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。

注意!自执行函数也会创建作用域,外部无法引用它内部的变量,这种机制可以避免变量污染。

13. 原型

  • 在JS中每当定义一个函数对象的时候,就会生成一个prototype,这个就是原型,原型里面存放的是想要共享的属性和方法
  • 实例对象和原型之间的链接就是_proto_,JS在创建对象的时候,都有一个叫做proto的内置属性,用于指向创建它的函数对象的原型对象prototype
  • 原型可以用来做什么?可以用来实现JS的类的继承

14. 原型链

  • 在引用一个对象的某个属性时,如果对象中没有该属性,那么它就会从_proto_所指向的创建它的对象的原型对象prototype中去查找,直到找到或者NULL为止

15. 继承

  • 原型继承
    • 子类的原型指向父类的新构造的实例
    • 缺点:共享父类原型上所有属性,但子类修改属性,父类属性也会被修改
  • 借用构造函数
    • 在子类中调用父类的构造方法,可向父类传参数
    • 缺点:只继承了父类构造函数的属性,没有继承原型,每个新实例都有父类的构造函数,构造函数无法复用
  • 组合继承
    • 在子类中调用父类的构造方法,并且让子类的prototype指向父类构造的新的实例
    • 缺点:调用了两次构造函数,消耗内存
  • 原型式继承
    • 封装一个函数容器,在容器内包装一个函数对象,使其的prototype指向继承传入的父类的实例,最后返回该对象;可以用object.create()
  • 寄生式继承
    • 把对函数的继承、新属性、方法的添加封装成一个函数
  • 寄生组合式继承
    • 解决组合式调用的缺点,在原型式继承的基础上,在子类中调用父类的构造函数,特别需要注意的是要在最后修复子类的构造函数指向自己的函数方法

16. ES6 ES7 ES8 -- 新特性

  • ES6
    • 模块化
    • 箭头函数
      • 箭头函数中的this指向的是定义时所在的环境,不是指向调用的对象
      • 箭头函数不能用于构造函数
      • 箭头函数没有prototype
      • 箭头函数不能通过call、bind、apply来改变this
    • 函数参数默认值
        function add(a, b = 1) {
            return a + b;
        }
        console.log(add(5, 2)); // expected output: 7
        console.log(add(5)); // expected output: 6
    
    • 模板字符串
        const name = '阿熊_'
        const sayHi = `hello ${name}`;
        console.log(sayHi); // hello 阿熊_
    
    • 解构赋值
        let name, age;
        [name, age] = ['阿熊_', 3];
        console.log(name, age); // 阿熊_ 3
    
    • 延展操作符
       const arr1 = [1,2,3];
       const arr2 = [4,5,6];
       const arr = [...arr1, ...arr2];
       console.log(arr); // [1,2,3,4,5,6]
    
    • 对象属性简写

    • Promise

    • Let Const 块级作用域

      var let const的区别

      • let const不存在变量提升,var存在变量提升
      • let const不能重复声明,var可以重复声明
      • let const有块级作用域,var没有块级作用域

      const声明的一个只读的常量,一旦声明,常量的值就不能改变

      const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动,对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(对象和数组),变量所指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是改变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

  • ES7
    • Array.prototype.includes()
    • 指数操作符
  • ES8
    • async/await
      • async是promise的语法糖封装
      • 以同步的方式写异步
      • await关键字可以暂停async function的执行
      • await关键字可以以同步的写法获取promise的执行结果
      • try-catch可以获取await所得到的错误
    • Object.values()
    • Object.entries()

17. this指向问题

  • 全局执行:指向window
  • 函数中执行:
    • 普通函数调用:一个函数被直接调用的时候,属于全局调用,这时候它的this指向全局对象
    • 作为对象的方法被调用:this指向当前的这个对象
    • 作为一个构造函数被使用:this指向了这个构造函数调用时候实例化出来的对象
    • 箭头函数:this对象,就是定义时所在的对象,而不是使用时所在的对象
    • call、apply、bind

18. addEventListener()和直接添加事件的区别

/**
 event(必须):事件名   
 function(必须):事件触发时执行的函数   
 useCapture(可选):布尔值,指定事件是否在捕获或冒泡阶段执行。默认为false(冒泡阶段执行),true(捕获阶段执行)
*/
addEventListener(event,function,useCapture)
  • addEventListener()方法添加的事件句柄不会覆盖已存在的事件句柄,即可以为同一个元素添加多个事件句柄,同类型的也可以,而直接添加事件添加同类型的事件句柄就会发生覆盖现象
  • addEventListener()可以向任何DOM对象添加事件监听,不仅仅是HTML元素,如window对象
  • addEventListener()方法可以更简单的控制事件(冒泡与捕获)

19. window.onload()和$(document).ready()的区别

  • window.onload是在页面中包含图片在内的所有元素全部加载完成再执行
  • $(document).ready()是DOM树加载完成之后执行,不包含图片,其他媒体文件
    因此$(document).ready()快于window.onload()执行

JS 手写代码题

1. 高阶函数: Map、Reducer、Filter

map: 一般用来对原数组进行某操作返回一个新数组,不改变原数组

/**
 参数一:当前元素
 参数二:当前元素索引
 参数三:调用的数组
*/
const arr = [1,2,3,4,5];
const new_arr = arr.map((current_value, current_index, arr) => {
    return current_value * current_index;
});
console.log(new_arr); // [0,2,6,12,20]

reducer: 一般用来让前项和后项进行某种操作,得到一个累积值,不改变原数组

/**
 参数一:累计器累计回调的返回值,是上一次调用回调时返回的累计值,或initial_value
 参数二:当前元素
 参数三:当前元素索引
 参数四:调用的数组
 参数五:作为第一次调用callback函数的第一个参数的值,如果没有提供初始值,则将使用数组中的第一个元素,在没有初始值的空数组上调用reduce将报错
*/
const arr = [1,2,3,4,5];
const res = arr.reduce((accumulator, current_value, current_index, arr, initial_value) => {
    return accumulator + current_value;
});
console.log(res); // 15

filter: 一般用来对原数组进行筛选,返回一个新的数组,不改变原数组

/**
 参数一:当前元素
 参数二:当前元素索引
 参数三:调用的数组
*/
const arr = [1,2,3,4,5,1,2,3,6,7,5,4,3];
const new_arr = arr.filter((current_value, current_index, arr) => {
    return arr.indexOf(current_value) === current_index;
});
console.log(new_arr); // [1,2,3,4,5,6,7]

2. 判断一个对象是否是数组

// method 1
Object.prototype.toString.call(obj) === "[object Array]";
// method 2
obj instanceof Array
// method 3
obj.constructor === Array
// method 4
Array.isArray(obj)

3. 数组扁平化

// 1. 递归
function flatten(arr) {
    let res;
    for(let i=0; i<arr.length; i++) {
        if(Array.isArray(arr[i]) {
            res = res.concat(flatten(arr[i]));
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}
// 2.reduce
function flatten(arr) {
    return arr.reduce((prev, next) => {
    	return prev.concat(Array.isArray(next) ? flatten(next) : next);
    },[])
}
// 3.扩展运算符
function flatten(arr) {
    while(arr.some(item => Array.isArray(item))) {
    	arr = [].concat(...arr);
    }
    return arr;
}

4. 设置扁平化的深度

function flatten(arr, deep) {
	while(arr.some(item => Array.isArray(item))) {
    	if(deep === 0) {
        	break;
        } else {
        	deep--;
            arr = [].concat(...arr);
        }
    }
    return arr;
}

5. 节流 -- 鼠标不断点击/监听滚动事件

// 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
function throttle(func, delay) {
    let last, deferTimer;
    return function(args) {
    	let that = this;
        let _args = args;
        let now = new Date();
        if(last && now-last < delay) {
        	clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
            	last = now;
                func.call(that, _args);
            }, delay);
        } else {
        	last = now;
            func.call(that, _args);
        }
    }
}

6. 防抖 -- 输入框输入/调整浏览器窗口大小

// 在事件被处罚n秒后再执行回调,如果在这n秒内又被触发,则重新计时
function debounce(func, delay) {
	return function(args) {
    	let that = _this;
        let _args = args;
        clearTImeout(func.id);
        func.id = setTimeout(() => {
        	func.call(that, _args)
        },delay);
    }
}

7. call apply bind底层实现

Function.prototype.myCall = function(obj) {
    obj.fn = this;
    let args = [...arguments].slice(1);
    obj.fn(...args);
    delete obj.fn;
}

Function.prototype.myApply = function(obj) {
    obj.fn = this;
    let args = [...arguments[1]];
    obj.fn(...args);
    delete obj.fn;
}

Function.prototype.myBind = function(obj) {
    let that = this;
    let args = [...arguments].slice(1);
    return function() {
    	return that.apply(obj, args.concat(...arguments));
    }
}

8. 深拷贝

function deepClone(target) {
	if(typeof target === 'object') {
    	let cloneTarget = Array.isArray(target) ? [] : {};
        for(let key in target) {
        	cloneTarget[key] = typeof target[key] === 'object' ? deepClone(target[key]) : target[key]
        }
        return cloneTarget;
    } else {
    	return target;
    }
}

9. 手写instancof

// 其实instanceof 主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可
function myInstancof(leftValue, rightValue) {
	let rightProto = rightValue.prototype;
    leftValue = leftValue.__proto__;
    while(true) {
    	if(leftValue == null) {
        	return false;
        }
        if(leftValue === rightProto {
        	return true;
        }
        leftValue = leftValue.__proto__
    }
}

10.手写new

let newMethod = function(Parent, ...rest) {
    // 1. 以构造器的prototype属性为原型,创建新对象
    let child = Object.create(Parent.prototype);
    // 2. 将this和调用参数传给构造器执行
    let result = Parent.apply(chidl, rest);
    // 3. 如果构造器没有手动返回对象,则返回第一步的对象
    return typeof result === 'object' ? result : child;
}

11.手写promise.all

Promise.prototype.all = function(promises) {
    let result = new Array();
    let count = 0;
    let length = promises.length;
    return new Promise((res, rej) => {
        let (let p of promises) {
            Promises.resolve(p).then((res) => {
                count++;
                result.push(res);
                // 当所有函数都成功执行了,resolve输出所有返回结果
                if (count === length) {
                    return resolve(result);
                }
            }, (err) => {
                // 若其中一个执行失败时,则返回err
                return reject(err);
            })
        }
    })
}

12.获取地址栏URL中的参数

URL中第一个?后的字符串即为传递的参数,但是有个特殊情况,即#后面的内容并不是传递的参数而是网页位置的标识符,所以如果URL中包含#时,只需要解析?#之间的字符串即可

const url = "https: //zhidao.baidu.com/question/1768422895052400180.html?fr=iks&word=slice&ie=gbk";
function parseUrlParams(url) {
   let queryStr = query ? query.split("?")[1] : location.search.slice(1);
   let req = new Object();
   
   queryStr = queryStr.split("#")[0];
   let arr = quertStr.split("&");
   
   for(let i=0; i< arr.length; i++) {
       let tmp = arr[i].split("=");
       let key = tmp[0];
       let val = tmp[1];
       req[key] = val;
   }
}

13.实现每隔一秒钟输出1,2,3...数字

function printNum() {
    for(let i=0; i<10; i++) {
        (function(j) {
            setTimeout(() => {
                console.log(j+1);
            }, j*1000)
        })(i)
    }
}