前端JS面试

Posted codedd

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端JS面试相关的知识,希望对你有一定的参考价值。

javascript

1、原始值和引用值类型和区别

原始值类型:Number、String、Boolean、Null、Undefined

引用值类型:Object、Array、Function、Date、RegExp

区别:原始值存储在栈中,引用值把引用变量存储在栈中,而实际的对象存储在堆中,每一个引用变量都有一个指针指向其堆中的实际对象

let a = 1;
let b = a;
a = 2;
console.log(b); // 1

原始变量赋值给另一个原始变量时,只是把栈中的内容复制给另一个原始变量,此时这两个原始变量互不影响

let a = [1,2,3,4];
let b = a;
a.push(5);
console.log(b); // [1,2,3,4,5]
a = [6];
console.log(b); // [1,2,3,4,5]

引用变量赋值给另一个引用变量时,各自的变量名存储在栈中,而实际对象的值指向堆中同一个地址,当变量a通过方法改变值时,实际上只改变堆中的内容,但地址不变,因此b的值也会改变;但是当变量a通过非方法改变值时,系统会为a重新创建一个堆区,a的指针指向新的堆地址,而b的指针仍然指向旧的堆地址

2、判断数据类型

1.typeof

typeof 对于原始数据类型除了 null 以外都能判断出来,但是对于引用数据类型,判断的结果都是 object

console.log(typeof 1); // number
console.log(typeof ‘1‘); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof null); // object
console.log(typeof function () {}); // function
console.log(typeof []); // object
console.log(typeof {}); // object

2.instanceof

a instanceof b 判断 b 的原型对象是否在 a 的原型链上

原理如下:

function instanceOf(a, b) {
    let bp = b.prototype;
    let ap = a.__proto__;
    while(true){
        if(ap === bp){
            return true;
        }else if(ap === null){
            return false;
        }
        ap = ap.__proto__;
    }
}

instanceof 主要用于判断引用数据类型

console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Object); // true

instanceof 也可以用来判断原始数据类型(null 和 undefined 除外)

let a = 1;
console.log(a instanceof Number); // false

为什么返回 false 呢?因为 instanceof 是 Object instanceof constructor,如果不是 Object 都返回 false

console.log(new Number(1) instanceof Number); // true
console.log(typeof new Number(1)); // Object

通过 new Number() 生成的实例就是 object

3.Object.prototype.toString.call()

利用 Object 原型对象上的 toString 方法可以精确的判断各种数据类型

let test = Object.prototype.toString;
console.log(test.call(1)); // [object Number]
console.log(test.call(‘1‘)); // [object String]
console.log(test.call(true)); // [object Boolean]
console.log(test.call(null)); // [object Null]
console.log(test.call(undefined)); // [object Undefined]
console.log(test.call([])); // [object Array]
console.log(test.call({})); // [object Object]

4.constructor

通过实例对象原型上的 constructor 属性来判断数据类型(null 和 undefined 除外)

console.log(1.constructor === Number); // true
console.log(true.constructor === Boolean); // true
console.log("1".constructor === String); // true
console.log([].constructor === Array); // true
console.log(function(){}.constructor === Function); // true
console.log({}.constructor === Object); // true

3、类数组与数组的区别与转换

类数组

  1. 拥有 length 属性,其他属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
  2. 不具有数组的方法
  3. 类数组是一个普通对象,而数组是 Array 类型

常见的类数组有:

  1. 函数的参数 arguments
  2. DOM 方法返回的结果
  3. jQuery 对象(比如 $(‘div‘))

类数组转换为数组

  1. Array.prototype.slice.call

    const divs = document.querySelectorAll(‘div‘);
    const newDivs = Array.prototype.slice.call(divs);
    
  2. 扩展运算符

    const divs = document.querySelectorAll(‘div‘);
    const newDivs = [...divs];
    
  3. Array.from

    const divs = document.querySelectorAll(‘div‘);
    const newDivs = Array.from(divs);
    

4、数组的常见API

isArray():判断是否为数组

const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true

toString():将数组转换为以逗号分隔的字符串

const arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3

join():返回按照指定字符分隔的字符串

const arr = [1, 2, 3];
console.log(arr.join(‘-‘)); //1-2-3

concat():用于连接两个或多个数组,该方法不会改变原数组,而仅仅会返回被连接数组的一个副本

const arr = [1, 2, 3];
const arr1 = [4, 5, 6];
console.log(arr.concat(arr1)); // [1,2,3,4,5,6]

slice(start, end):截取数组中的元素,该方法不会改变原数组,而是返回一个子数组

start 和 end均为空时,截取数组所有元素

const arr = [1, 2, 3];
console.log(arr.slice()); // [1,2,3]

end 为空时,从 start 开始截取到数组结尾

const arr = [1, 2, 3];
console.log(arr.slice(1)); // [2,3]

end 不为空时,从 start 开始截取到 end 的前一位

const arr = [1, 2, 3];
console.log(arr.slice(1, 2)); // [2]

start 和 end 为负数时,从数组末尾开始计算

const arr = [1, 2, 3];
console.log(arr.slice(-3, -1)); // [1,2]

reverse():翻转数组

const arr = [1, 2, 3];
console.log(arr.reverse()); // [3,2,1]

sort():自定义排序

const arr = [1, 2, 3];
console.log(arr.sort(function (a, b) {
    //return a - b; 从小到大排序
    return b - a; // 从大到小排序
}));

splice():向数组中添加/删除元素,该方法会改变原数组

参数 描述
index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
howmany 必需。要删除的项目数量。如果设置为 0,则不会删除项目。
item1, ..., itemX 可选。向数组添加的新项目。

添加元素

const arr = [1, 2, 3];
arr.splice(3,0,4,5);
console.log(arr); // [1,2,3,4,5]

删除元素

const arr = [1, 2, 3];
arr.splice(2,1);
console.log(arr); // [1,2]

替换元素

const arr = [1, 2, 3];
arr.splice(2,1,4);
console.log(arr); // [1,2,4]

push():向数组末尾添加一个或多个元素,并返回新的长度

const arr = [1, 2, 3];
console.log(arr.push(4)); // 4
console.log(arr); // [1,2,3,4]

unshift():向数组开头添加一个或多个元素,并返回新的长度

const arr = [1, 2, 3];
console.log(arr.unshift(0)); // 4
console.log(arr); // [0,1,2,3]

pop():删除数组末尾的元素

const arr = [1, 2, 3];
arr.pop();
console.log(arr); // [1,2]

shift():删除数组开头的元素

const arr = [1, 2, 3];
arr.shift();
console.log(arr); // [2,3]

entries():返回数组的可迭代对象

const arr = [‘张三‘, ‘赵四‘, ‘王五‘];
let iterator = arr.entries();
for(let v of iterator){
    console.log(v);
}
/*
	[0, "张三"]
	[1, "赵四"]
	[2, "王五"]
*/

every():检测数组的每个元素是否都符合条件,不会对空数组进行检测,不会改变原数组

const arr = [1, 2, 3];
const result = arr.every(item => item > 0   );
console.log(result); // true

fill():用一个固定值来填充数组

const arr = [1, 2, 3];
arr.fill(6);
console.log(arr); // [6,6,6]

filter():检测数组元素,并返回符合条件的所有元素的数组,不会对空数组进行检测,不会改变原数组

const arr = [1, 2, 3];
console.log(arr.filter(item => item>1)); // [2,3]

find():返回数组中符合条件的第一个元素,空数组不会执行,不会改变原数组

const arr = [1, 2, 3];
console.log(arr.find(item => item>1)); // 2

findIndex():返回数组中符合条件的第一个元素的索引,空数组不会执行,不会改变原数组,如果不存在,则返回 -1

const arr = [1, 2, 3];
console.log(arr.findIndex(item => item > 1)); // 1
console.log(arr.findIndex(item => item > 4)); // -1

forEach():遍历数组

const arr = [1, 2, 3];
arr.forEach(item => {
    console.log(item);
})

from():从一个类数组或可迭代对象创建一个新的浅拷贝的数组

console.log(Array.from(‘foo‘));
// ["f", "o", "o"]

console.log(Array.from([1, 2, 3], x => x + x));
// [2,4,6]

flat():按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

扁平化嵌套数组

const arr1 = [1, 2, [3, 4]];
arr1.flat(); 
// [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

扁平化并移除数组空项

const arr4 = [1, 2, , 4, 5];
arr4.flat();
// [1, 2, 4, 5]

includes():用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false

const arr = [1,2,3];
console.log(arr.includes(1)); // true

indexOf():返回数组中给定元素的第一个索引,如果不存在,则返回-1

const arr = [1,2,3,1];
console.log(arr.indexOf(1)); // 0
console.log(arr.indexOf(1,2)); // 3
console.log(arr.indexOf(4)); // -1

reduce():对数组中的每个元素执行一个提供的函数(升序执行),并将结果汇总为单个返回值

const arr = [1,2,3,4,5,6,7,8,9,10];
// 累加必须提供初始值
console.log(arr.reduce((acc, cur) => acc + cur, 0)); // 55

map():创建一个新数组,其结果是该数组中的每个元素调用提供的函数后的返回值

const arr = [1,2,3];
console.log(arr.map(item => item*2)); 
// [2,4,6]

考点:

通常情况下,map 方法中的 callback 函数只需要接受一个参数,就是正在被遍历的数组元素本身。但这并不意味着 map 只给 callback 传了一个参数。例如:

const arr = [‘1‘,‘2‘,‘3‘];
console.log(arr.map(parseInt));

期望输出 [1,2,3],然而实际结果是 [1,NaN,NaN]

parseInt 经常被带着一个参数使用, 但是这里接受两个。第一个参数是一个表达式,第二个是callback function的基,Array.prototype.map 传递3个参数:

  • the currentValue
  • the index
  • the array

第三个参数被parseInt忽视了, 但第二个参数会被使用

参数 描述
string 必需。要被解析的字符串。
radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

上述的迭代步骤为:

// parseInt(string, radix) -> map(parseInt(value, index))
/* 1 */ parseInt(‘1‘,0); // 1
/* 2 */ parseInt(‘2‘,1); // NaN 
/* 3 */ parseInt(‘3‘,2); // NaN 二进制只有0和1

解决方案:

function returnInt(element) {
  return parseInt(element, 10);
}

[‘1‘, ‘2‘, ‘3‘].map(returnInt); // [1, 2, 3]
// 指定基数即进制为10

// 只给parseInt传入当前元素值
[‘1‘, ‘2‘, ‘3‘].map( item => parseInt(item) );

[‘1‘, ‘2‘, ‘3‘].map(Number); // [1, 2, 3]

参考 MDN Array

5、bind、call、apply的区别

bind():该方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值

语法:

var fn = Function.bind(obj, [param1[,param2][,...paramN]])

使用场景为函数不需要立即调用,但又想改变函数内部的 this 指向(比如定时器内部的 this)

const btn = document.querySelector(‘button‘);
btn.onclick = function () {
    this.disabled = true;
    setTimeout(function () {
        this.disabled  =false;
    }.bind(this), 2000);
}

bind() 主要是为了改变函数内部的 this 指向

apply():apply() 方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组(参数数组可以是数组实例,也可以是 arguments 对象)

语法:

Function.apply(obj, args)
// args将作为参数传递给Function

使用场景主要与数组有关

1.Math.max 实现得到数组的最大项

const arr = [1,2,3];
console.log(Math.max.apply(Math, arr)); // 也可以使用null,但严格模式下还是要使用Math
// 3

2.Array.prototype.push 实现合并两个数组

const arr1 = [1,2,3];
const arr2 = [4,5,6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1);
// [1,2,3,4,5,6]

call():call() 方法与 apply() 方法类似,接收参数的方式有些不同,第一个参数为在其中运行函数的作用域,其余参数都直接传递给函数,即传递给函数的参数必须逐个列举出来

语法:

Function.call(obj, [param1[,param2][,...paramN]])
// param参数列表会直接传递给Function

使用场景是可以实现继承

function Person(name, age) {
    this.name = name;
    this.age = age;
}
function Student(name, age, id) {
    Person.call(this, name, age);
    this.id = id;
}
let s = new Student(‘张三‘, 20, ‘007‘);
console.log(s);
// Student?{name: "张三", age: 20, id: "007"}

6、new 的原理

  1. 在内存中创建一个空对象
  2. 给这个空对象添加属性和方法
  3. 将构造函数的 this 指定为创建的新对象,并将参数传入
  4. 如果构造函数没有手动返回对象,则返回第一步创建的对象,如果构造函数有返回对象,则 this 指向构造函数返回的对象

实现原理如下:

function Person(name) {
    this.name = name;
}
function newObject(parent, ...args) {
    let child = {};
    child.__proto__ = parent.prototype;
    parent.apply(child, args);
    return child;
}
const p = newObject(Person, ‘张三‘);
console.log(p);// 张三
Person.prototype.sayName = function () {
    console.log(‘我叫‘+this.name);
};
p.sayName(); // 我叫张三

7、如何正确判断 this 的指向

this:谁调用它,this 就指向谁

1.普通函数:this 指向 window,严格模式下(‘use strict‘)会抛出错误 undefined

// ‘use strict‘;
var name = ‘张三‘;
function fn() {
    console.log(this.name);
}
fn();

2.对象函数:this 指向该函数所属对象

var obj = {
    sayHello(){
        console.log(this);
    }
}
obj.sayHello();
// {sayHello: f}

3.构造函数:如果构造函数没有返回对象,则 this 指向创建的对象实例;如果构造函数有返回对象,则 this 指向返回的对象

function Person(name, age) {
    this.name = name;
    this.age = age;
}
var p = new Person(‘张三‘, 20);
console.log(p);
// Person?{name: "张三", age: 20}
function Person(name, age) {
    this.name = name;
    this.age = age;
    let obj = {
        name: ‘赵四‘,
        age: 18
    };
    return obj;
}
var p = new Person(‘张三‘, 20);
console.log(p);
// {name: ‘赵四‘, age: 18}

4.绑定事件函数:this 指向事件的调用者

var btn = document.querySelector(‘button‘);
btn.onclick = function () {
    console.log(this);
}
// <button>点击</button>

5.定时器函数:this 指向 window

setTimeout(function () {
    console.log(this);
},1000);

6.立即执行函数:this 指向 window

(function() {
    console.log(this);
})();

7.箭头函数:不绑定 this,this 指向函数定义位置的上下文

btn.onclick = function () {
    setTimeout(() => {
        console.log(this);
        // <button>点击</button>
    },1000)
}
// 通过箭头函数可以改变定时器函数的this指向

8.显式绑定:函数通过 call()、apply()、bind()方法绑定,this 指向方法中传入的对象

function fn() {
    console.log(this);
}
var person = {
    name: ‘张三‘
};
fn.call(person);
fn.apply(person);
fn.bind(person)();
// {name: ‘张三‘}

如果这些方法中传入的第一个参数是 undefined 或 null,严格模式下 this 指向传入的值 undefined 或 null;非严格模式下 this 指向 window

function fn() {
    console.log(this);
}
fn.call(null);// window
‘use strict‘;
function fn() {
    console.log(this);
}
fn.call(null); // null

9.隐式绑定:函数的调用时在某个对象上触发的,即调用位置存在上下文对象(相当于对象函数中的 this 指向)典型的隐式绑定为 xxx.fn()

function fn() {
    console.log(this.name);
}
var person = {
    name: ‘张三‘,
    fn
};
person.fn(); // 张三

8、变量提升与函数提升

变量提升:将变量的声明提升到它所在作用域的顶端去执行,将赋值放在代码所在的位置(注意只有 var

才存在变量提升)

console.log(a);
var a = 1; // undefined

上述代码的实际执行顺序如下:

var a;
console.log(a);
a = 1;

而如果先进行赋值:

a = 1;
var a;
console.log(a); // 1

声明提升到顶端,所以输出1

console.log(‘1-‘+v1);
var v1 = 100;
function foo() {
    console.log(‘2-‘+v1);
    var v1 = 200;
    console.log(‘3-‘+v1);
}
foo();
console.log(‘4-‘+v1);
// 1-undefined
// 2-undefined
// 200
// 100

函数提升:函数提升是整个代码块提升到它所在作用域的顶端执行

console.log(fn);
function fn () {
    console.log(1);
}
/*
? fn () {
        console.log(1);
    }
*/

执行顺序相当于:

function fn () {
    console.log(1);
}
console.log(fn);

函数提升存在函数优先原则:

foo(); //1
var foo;
function foo () {
    console.log(1);
}
foo = function () {
    console.log(2);
}

9、作用域与作用域链、执行上下文

作用域:变量在某个范围内生效,目的是为了提高程序的安全性,减少命名冲突,分为全局作用域和局部作用域

  • 全局作用域:script 标签,或者整个 js 文件
    • 全局变量:
      1. 在全局作用域下的变量
      2. 在函数内部没用声明,直接赋值
      3. 只有在浏览器关闭时才会销毁,消耗内存
  • 局部作用域:函数内部,变量只在函数内部生效
    • 局部变量:
      1. 在局部作用域下的变量
      2. 函数的形参可以看做局部变量
      3. 当程序执行完毕就会销毁,节约内存资源

作用域链:一般情况下,变量的取值是到创建该变量的函数作用域下查找,但是如果在当前作用域下没有查找到,就会向上一级作用域查找,直到全局作用域,这样一个查找过程形成的链称为作用域链。作用域链相当于内部函数访问外部函数的变量,采取的是链式查找的方式来决定取哪个值。

//第一种情况,当函数作为参数
var x = 10;
function show(callback) {
    var x = 5;
    callback && callback();
}
function fun() {
    console.log(x);// 10 函数fun的上级作用域是全局作用域
}
show(fun);
//第二种情况,当函数作为返回值输出
var x = 10;
function show() {
    var x = 5;
    return function() {
        console.log(x);// 5 函数的上级作用域是show函数
    }
}
var res = show();
res();

执行上下文:当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被提升,有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文,JavaScript 运行任何代码都是在执行上下文中运行

执行上下文分类:

  • 全局执行上下文:不在任何函数中的代码都位于全局执行上下文中,代码首先进入的环境
  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文,相当于该函数被调用时执行的环境
  • eval 函数执行上下文:运行在 eval 函数中的代码也有自己的函数执行上下文(不常用)

执行上下文栈:也叫调用栈,执行上下文栈用于存储代码运行期间创建的所有执行上下文,JS 代码首次运行,先创建一个全局执行上下文并压入栈中,之后每次函数调用,都会创建一个函数执行上下文并压入栈中,当函数调用完成后,这个函数执行上下文以及其中的数据都会被销毁,然后重新进入全局执行上下文

var a = 10 ;                     // 1.进入全局上下文环境
var fun;
var bar = function (x) {
    var b = 10;
    fun(x + b);              // 3.进入fun上下文环境
}
fun = function (y) {
    var c = 20;
    console.log(y + c);
}
bar(5);                          // 2.进入bar上下文环境

执行上下文的生命周期:

  1. 创建阶段
    1. 创建变量:首先初始化函数的参数 arguments,提升函数声明和变量声明(预解析)
    2. 创建作用域链:作用域链本身包含变量,作用域链用于解析变量,当被要求解析变量时, JS 始终从代码嵌套的最内层开始查找,如果最内层没有找到,则会向上一级作用域查找,直到找到该变量
    3. 确定 this 的指向
  2. 执行阶段:变量赋值,代码执行
  3. 回收阶段:执行上下文出栈等待虚拟机回收执行上下文

执行上下文特点:

  1. 单线程,在主进程上运行
  2. 同步执行,从上往下按顺序执行
  3. 全局执行上下文只有一个,浏览器关闭时会被弹出栈(销毁)
  4. 函数执行上下文没有数目限制
  5. 函数每被调用一次,都会创建一个新的执行上下文环境
  6. 函数调用完毕时,函数执行上下文以及其中的数据都会被销毁

10、闭包及其作用

高阶函数

高阶函数时对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值;JavaScript 的回调函数是以实参形式传入其他函数中,也属于高阶函数

// 1.将函数作为参数(回调函数)
function fn(callback) {
    callback && callback();
}
fn(function () {
    console.log(‘hello‘);
})

// 2.将函数作为返回值
function fun() {
    return function () {
        console.log(‘world‘);
    }
}
fun()();

变量作用域

  1. 函数内部不可以访问全局变量
  2. 函数外部不可以访问局部变量
  3. 当函数执行完毕,本作用域内的局部变量会被销毁

闭包

闭包指有权访问另一个函数作用域中的变量的函数,闭包允许函数访问局部作用域之外的数据,即使外部函数已经退出,外部函数中的变量仍然可以被内部函数访问到,闭包的主要作用:延伸了变量的作用范围

闭包实现的三个条件:

  1. 内部函数访问外部函数的变量
  2. 外部函数已经退出
  3. 内部函数仍然可以访问
function fn() {
    var a = 1;
    return function (b) {
        a = a + b;
        console.log(a);
    }
}
var f = fn();
f(1); // 2
f(2); // 4

上述函数执行的时候,f 得到的是闭包对象的引用,fn 函数执行完毕退出,但是 fn 函数中的活动对象由于闭包的存在并没有被销毁,执行 f 函数仍然可以访问到 a 变量,而执行 f(2)后 a 变量的值为4,因为闭包的引用,f 并没有消除

闭包的核心内容:有些情况下(函数调用返回一个函数),函数调用完成之后,其执行上下文环境不会被销毁,所以使用闭包会增加内存开销,在 IE 中可能导致内存泄露,解决方法:在退出函数之前,将不使用的局部变量全部清除(变量赋值为null)

11、原型和原型链

原型:在 JavaScript 中,每一个函数都有一个 prototype 对象属性,指向另一个对象(原型对象),prototype 的所有属性和方法都会被构造函数的实例所继承。所以,我们可以把那些公共不变的方法,直接定义在 prototype 对象属性上 (一般情况下,公共属性定义在构造函数里,公共方法定义在原型对象上)

原型链:JavaScript 成员查找机制是按照原型链来查找的(就近原则)

  1. 当访问一个对象的属性(或方法)时,首先查找对象是否拥有该属性(或方法)
  2. 如果没有,就找它的原型(__proto__)指向的构造函数的原型对象(prototype)
  3. 如果还没有,就找原型对象的原型指向的 Object 的原型对象
  4. 以此类推,递归访问 __proto__,直到找到,找不到则为 null

12、prototype 与 __proto__ 的关系与区别

prototype(显式原型属性):只有函数对象才具有 prototype 属性,这个属性指向一个对象,这个对象包含所有实例共享的属性和方法,这个对象也有一个属性 constructor,指回原构造函数

__proto__(隐式原型属性):所有对象都具有该属性,指向构造该对象的构造函数的原型

13、继承的实现方式及比较

父类:

function Parent(name) {
    this.name = name;
    this.arr = [1,2,3];
}
Parent.prototype.showName = function () {
    console.log(this.name);
}

1.原型继承:子类构造函数的原型等于父类构造函数的实例

function Child() {}
Child.prototype = new Parent();
var c = new Child();
console.log(c);

优点:实例可以继承构造函数的属性,父类构造函数的属性,父类构造函数原型的属性

缺点:

  1. 实例无法向父类构造函数传参

  2. 所有实例都会共享父类的引用类型属性

    var c = new Child();
    var c1 = new Child();
    c.age = 20;
    c.arr.push(4);
    console.log(c1.arr); // [1,2,3,4]
    

2.借用构造函数:利用 call() 方法将父类构造函数引入子类构造函数

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
var c = new Child(‘张三‘, 20);
console.log(c);

优点:

  1. 实例可以向父类构造函数传参
  2. 父类原型属性不会共享

缺点:

  1. 只能继承父类构造函数的属性,不能继承原型属性
  2. 无法实现构造函数的复用(每次实例化都会重新调用)

3.组合继承:将原型继承和借用构造函数继承组合(常用)

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
Child.prototype = new Parent();
var c = new Child(‘张三‘, 20);
console.log(c);
c.showName();

优点:

  1. 可以继承父类原型上的属性,实例可以向父类构造函数传参,构造函数可以复用
  2. 每个实例引入的构造函数属性是私有的

缺点:

  1. 调用了两次父类构造函数(消耗内存)
  2. 子类构造函数会代替原型上的父类构造函数

4.原型式继承:将一个函数的原型指向父类实例,然后返回这个函数的对象

function Child(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}
var p = new Parent(‘张三‘);
var c = new Child(p);
console.log(c);

缺点:

  1. 所有实例都会共享父类的引用类型属性
  2. 子类实例化对象时无法传参

5.寄生式继承:给原型式继承再嵌套一层,实现传参

function Child(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}
var p = new Parent(‘张三‘);
// 以上是原型式继承
function ChildObject(obj, age) {
    var c = Child(obj);
    c.age = age;
    return c;
}
var c2 = new ChildObject(p, 20);
console.log(c2);

缺点:所有实例都会共享父类的引用类型属性

6.寄生组合式继承:寄生+组合实现继承

function Child(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}
var c = new Child(Parent.prototype);
function ChildObject(name, age) {
    Parent.call(this, name);
    this.age = age;
}
ChildObject.prototype = c;
c.constructor = ChildObject;
var co = new ChildObject(‘赵四‘, 20);
console.log(co);

优点:

  1. 可以多重继承
  2. 解决调用两次父类构造函数的问题
  3. 解决实例共享父类引用类型属性的问题

深拷贝与浅拷贝

区分浅拷贝与深拷贝:假设 B 复制了 A,如果修改 B,A 也发生变化,就是浅拷贝;如果 A 没有发生变化,就是深拷贝

实现浅拷贝

1.for...in 循环赋值(只能拷贝第一层)

function simpleCopy(obj1) {
    let obj2 = Array.isArray(obj1) ? [] : {};
    for(let k in obj1){
        obj2[k] = obj1[k];
    }
    return obj2;
}
let obj1 = {
    a: 1,
    b: 2,
    c: {
        d: 3
    }
};
let obj2 = simpleCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 1
console.log(obj1.c.d); // 4

2.Object.assign

let obj2 = Object.assign(obj1);
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 3
console.log(obj1.c.d); // 4

3.直接用 =赋值

let obj2 = obj1;
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 3
console.log(obj1.c.d); // 4

实现深拷贝

1.递归拷贝所有层级属性

let obj1 = {
    a: 1,
    b: 2,
    c: {
        d: [1,2,3]
    },
    f: function () {
        console.log(‘f‘);
    }
};
function deepCopy(obj1) {
    let obj2 = Array.isArray(obj1) ? [] : {};
    for(let k in obj1){
        if(typeof obj1[k] === "object"){
            obj2[k] = deepCopy(obj1[k])
        }else{
            obj2[k] = obj1[k];
        }
    }
    return obj2;
}
let obj2 = deepCopy(obj1);
console.log(obj2);
obj2.c.d.push(4);
console.log(obj1.c.d); // [1,2,3]

2.通过 JSON 对象来实现深拷贝

function deepCopy(obj1) {
    let obj = JSON.stringify(obj1);
    return JSON.parse(obj);
}
let obj2 = deepCopy(obj1);

缺点:

  1. 不能复制 function、正则、Symbol
  2. 循环引用报错
  3. 相同的引用会被重复复制

3.通过jQuery的extend方法实现深拷贝

var array = [1,2,3,4];
var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝

4.lodash函数库实现深拷贝

let result = _.cloneDeep(test)

14、函数防抖和节流

函数防抖和节流是为了解决用户在某一时间内频繁提交请求,给服务器造成压力的情况

函数防抖:在一定时间内,连续触发同一事件,只执行一次(只在最后一次执行或第一次执行)

定时器实现 :

// 只在最后一次执行
function debounce(fn, delay) {
    let timer = null;
    return () => {
        if(timer){
            //第一次触发时不会执行,后续触发时会清除定时器
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, delay)
    }
}

// 只在第一次执行
function debounce(fn, delay) {
    let timer = null;
    return () => {
        if(timer){
            clearTimeout(timer);
        }
        let callNow = !timer; // true
        // 后续如果没有触发,则将timer初始化为null
        timer = setTimeout(() => {
            timer = null;
        }, delay);
        if(callNow){
            fn.apply(this, arguments);
        }
    }
}

时间戳实现:

function debounce(fn, delay) {
    let pre = 0;
    return () => {
        let now = new Date();
        if(now - pre > delay){
            fn.apply(this, arguments);
        }
        pre = now;
    }
}

函数节流:在单位时间内,连续触发同一事件,只执行一次

定时器实现:

function throttle(fn, delay) {
    let flag = true;
    return () => {
        if(!flag) return; // flag为false时,直接返回
        flag = false;
        setTimeout(() => {
            fn.apply(this, arguments);
            // 执行完fn函数再将flag重置为true
            flag = true;
        }, delay)
    }
}

// 或者
function throttle(fn, delay) {
    let timer = null;
    return () => {
        if(!timer){ // timer为null时才设置定时器
            timer = setTimeout(() => {
                //执行完fn函数后将timer重置为null
                fn.apply(this, arguments);
                timer = null;
            },delay)
        }
    }
}

时间戳实现:

function throttle(fn, delay) {
    let pre = 0;
    return () => {
        let now = new Date();
        if(now - pre > delay){
            fn.apply(this, arguments);
            pre = now;
        }
    }
}

15、DOM常见的操作方式

1.查找节点

document.querySelector(selectors)
//接受一个CSS选择器为参数,返回第一个匹配该选择器的元素节点
document.querySelectorAll(selectors)
//接受一个CSS选择器为参数,返回所有匹配该选择器的元素节点
document.getElementById(id)
//返回匹配指定id属性的元素节点

2.生成节点

document.createElement(tagName) 
// 用来生成html元素节点
document.createTextNode(text) 
// 用来生成文本节点
document.createAttribute(name) 
// 生成一个新的属性对象节点,并返回它

3.事件操作

document.addEventListener(type,listener,capture) // 注册事件
document.removeEventListener(type,listener,capture) // 注销事件

4.节点操作

Node.appendChild(node) 
// 向节点添加最后一个子节点
Node.hasChildNodes() 
// 返回布尔值,表示当前节点是否有子节点
Node.cloneNode(true); 
// 默认为false(克隆节点), true(克隆节点及其属性,以及后代)
Node.insertBefore(newNode,oldNode) 
// 在指定子节点之前插入新的子节点
Node.removeChild(node) 
// 删除节点,在要删除节点的父节点上操作
Node.replaceChild(newChild,oldChild) 
// 替换节点

以上是关于前端JS面试的主要内容,如果未能解决你的问题,请参考以下文章

前端开发中最常用的JS代码片段

关于js----------------分享前端开发常用代码片段

程序员面试京东前端,现场JS代码写出魔方特效,成功搞定20K月薪

前端面试CSS系列——DIV垂直水平居中

回归 | js实用代码片段的封装与总结(持续更新中...)

前端面试js题