前端面试——JS进阶

JS进阶

1. ES6 基础知识点

变量提升

问:什么是变量提升?为什么存在变量提升

  • 函数及变量的声明都将被提升到函数的最顶部。也就是变量可以先使用再声明。
  • 变量提升的根本原因就是为了解决函数之间互相调用的情况。

问:变量和函数怎么进行提升的? 优先级是怎么样的?

  • 第一阶段:对所有的函数声明进行提升(忽略表达式和箭头函数),引用类型的赋值分为三步:
    • 开辟堆空间
    • 存储内容
    • 将地址赋值给变量
  • 第二阶段:对所有的变量进行提升,全部赋值为undefined,然后依次顺序执行代码(let和const时,不能在声明之前使用变量,这叫做暂时性死区)

var、let、const

问:var、let、const 三者的区别是什么

  • var 存在变量提升,而let、const则不会
  • var 在浏览器环境下声明的变量会挂载到window上,而其他两者不会。
  • let 和 const 的作用基本一致,后者声明的变量不能再次赋值(但是能改变值)

2. map、filter、reduce、foreach区别

map()

map(function(element,index,arr), thisValue)

传入一个函数,该函数会遍历数组,对每一个元素做变换之后,返回一个新数组

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • 可选。用作 “this” 的值。如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
  let arr = [2, 3, 4]
  let arr1 = arr.map(function (element, index, arr) {
     
    return arr[index] + 1
  }) 
  let arr2 = arr.map(function (element, index, arr) {
     
    return element + 1
  }) 
  console.log(arr); //  [2, 3, 4]
  console.log(arr1); // [3, 4, 5]
  console.log(arr2); // [3, 4, 5]

filter()

filter(function(currentValue,index,arr), thisValue)

传入一个函数,函数返回值为布尔类型,将返回值为真的元素放入新数组,返回这个新数组

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • thisValue: 可选。用作 “this” 的值。如果省略了 thisValue ,“this” 的值为 “undefined”
let arr = [1, 2, 3, 4]
let arr1 = arr.filter(function(element) {
     
  return element < 3
})
console.log(arr); //  [1, 2, 3, 4]
console.log(arr1); // [1, 2]

reduce()

reduce(function(total, element, index, arr), initialValue)

传入一个函数,返回一个值

  • total: 累计值(第一次的值代表初始化的值)

  • element: 对应数组的每个元素

  • index: 数组元素的下标

  • arr: 原数组

  • initialValue: 可选。传递给函数的初始值

let arr = [1, 2, 3]
let sum = arr.reduce(function(acc, element) {
     
  return acc + element
}, 1)
console.log(arr); // [1, 2, 3]
console.log(sum); // 7

forEach()

forEach(function(element, index, arr), thisValue)

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • initialValue: 可选。如果省略了 thisValue ,“this” 的值为 “undefined”

传入一个函数,直接操作原数组 没有返回值

let arr = [1, 3, 5]
let arr1 = arr.forEach(function(element, index, arr) {
     
  arr[index] = element+1
})
console.log(arr); // [2, 4, 6]
console.log(arr1); // undefined

3. 箭头函数

箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?

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

  • 函数体内的this, 指向定义时所在的对象,而不是使用时所在的对象
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数
  • 不可以使用new命令,因为它没有自己的this和prototype属性

4. this

this指向

  • 对象调用,this指向该对象(谁调用this就指向谁)

    var obj = {
            
        name:'小鹿', 
        age: '21', 
        print: function(){
            
            console.log(this) 
            console.log(this.name + ':' + this.age) 
        } 
    }// 通过对象的方式调用函数 obj.print(); // this 指向 obj
    
  • 直接调用的函数,this指向的是全局window对象

    function print(){
            
        console.log(this); 
    }// 全局调用函数 
    print(); // this 指向 window
    
  • 通过new的方式,this永远指向新创建的对象

    function Person(name, age){
            
        this.name = name; 
        this.age = age; 
        console.log(this); 
    }
    var xiaolu = new Person('小鹿',22); // this = > xaiolu
    
  • 箭头函数中的this

    由于箭头函数没有单独的 this 值。箭头函数的 this 与声明所在的上下文相同。也就是说调用箭头函数的时候,不会隐式的调用 this 参数,而是从定义时的函数继承上下文。

    const obj = {
            
        a:()=>{
            
            console.log(this); 
        } 
    }
    // 对象调用箭头函数 
    obj.a(); // window
    

如何改变this指向

我们可以通过调用函数的call、apply、bind来改变this的指向

var obj ={
     
    name:'zhangsan',
    age:18
}
function print() {
     
    console.log(this); //打印this的指向
    console.log(arguments); // 打印传递的参数
}
// 通过call 改变this指向
print.call(obj,1,2,3);

// 通过 apply 改变this 指向
print.apply(obj,[1,2,3]);

// 通过 bind 改变this的指向
let fn = print.bind(obj, 1,2,3);
fn();

再说一说这三者的共同点和不同点

共同点:

  • 功能角度:三者都能改变this指向,且第一个传递的参数都是this指向的对象
  • 传参角度:三者都采用的后续传参的方式

不同点:

  • 传参方面:call的传参是单个传递的,而apply后续传递的参数是数组形式(传单个值会报错),而bind没有规定,传递值和数组都可以
  • 执行方面:call和apply函数的执行是直接执行的,而bind函数会返回一个函数,然后我们想要调用的时候才会执行。

主要应用场景:

  1. call 经常用于继承
  2. apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值
  3. bind 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this), 他们的第一个参数会被忽略

5. new

创建对象的几种方式?

  • 字面量
  • new
  • Object.create()

字面量

var obj={
     
    name:'lxy'
}
  • 代码量更少,更易读
  • 对象字面量运行速度更快。它们可以在解析的时候被优化,他不会像 new 一个对象一样,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object() 的函数就执行,如果没找到,就继续顺着作用域链往上找,直到找到全局Object() 构造函数为止
  • Object()构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的

new

问:new 内部发生了什么过程?可不可以手写实现一个 new 操作符?

对于new关键字,我们第一想到的就是在面向对象中new 一个实例对象,但是在JS中new和Java中的new的机制不一样

一般Java中,声明一个构造函数,通过new 类名() 来创建一个实例对象,而这个构造函数是一种特殊的函数。但是在JS中,只要new一个函数,就可以new一个对象,函数和构造函数没有任何的区别

对于new创建对象:

var arr = new Array();

new 的过程包括一下四个阶段:

  • 创建一个新对象。
  • 这个新对象的_proro_ 属性指向原函数的prototype属性。(即继承原函数的原型)
  • 将这个新对象绑定到此函数的this上
  • 返回新对象,如果这个函数没有返回其他对象

Object.create(null)

对于Object.create() 方式创建对象:

Object.create(proto,[propertiesObject]);
  • proto: 新创建对象的原型对象。

  • propertiesObject: (可选) 可为创建的新对象设置属性和值。

  • 一般用于继承:

    var People= function(name) {
           
        this.name = name;
    }
    People.peototype.sayName= function() {
           
        console.log(this.name);
    }
    function Person(name, age) {
           
        this.age = age;
        People.call(this, name); // 使用call, 实现了People属性的继承
    };
    // 使用Object.create()方法,实现People原型方法的继承,并且修改了constructor 指向
    Person.prototype = Object.create(People.peototype, {
           
        constructor:{
           
            configurable:true,
            enumerable: true,
            value:Person,
            writable:true
        }
    });
    Person.prototype.sayAge = function() {
           
        console.log(this.age);
    }
    var p1 = new Person('person1',25);
    
    p1.sayName() //'person1'
    p1.sayAge(); // 25
    

三者创建对象的区别

  • new 和字面量创建的对象的原型指向Object.prototype, 会继承object的属性和方法。
  • 而通过Object.create(null) 创建的对象,其原型指向null, null作为原型链的顶端,没有也不会继承任何属性和方法。

6. 闭包

什么是闭包?

闭包就是能够访问其他函数内部变量的函数

闭包的作用

  • 访问其他函数内部变量
  • 保护变量不被内存回收机制回收
  • 避免全局变量被污染,方便调用上下文的局部变量加强封装性

闭包的缺点

闭包长期占用内存,内存消耗很大,可能导致内存泄漏

如何避免闭包引起的内存泄漏

  • 在退出函数前,将不使用的局部变量全部删除。可以使变量赋值为null;

7. 内存泄漏,垃圾回收机制

什么是内存泄漏?

不再用到的内存,没有及时释放,就叫做内存泄漏。

为什么会导致内存泄漏?

**内存泄漏是指我们已经无法再通过js代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。**导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃

垃圾回收机制

问:怎么解决内存泄漏?说一说JS垃圾回收机制的运行原理?

需要我们手动管理好内存,但是对于JS有自动垃圾回收机制,自行对内存进行管理

两种垃圾回收策略

垃圾回收器主要的功能就是每隔一段时间,就去周期性的执行收集不再继续用到的内存,然后将其释放掉

标记清除法

它的实现原理就是通过判断一个变量是否再执行环境中被引用,来进行标记删除

引用计数法

引用计数的最基本的含义就是跟踪记录每个值被引用的次数。

  • 缺陷:两个对象的互相循环引用,在函数执行完成的时候,两个对象相互的引用计数并未归0,而是依然占据内存,无法回收,当函数执行多次时,内存占用就会变多,导致大量的内存得不到回收。

8. 原型链

原型:

每个JS对象都有_proto_ 属性,这个属性指向了原型

原型链:

原型链就是多个对象通过_proto_ 的方式连接了起来形成一条链

总结:

  • 所有的实例的_proto_ 都指向该构造函数的原型对象(prototype)。
  • 所有的函数(包括构造函数) 是Function() 的实例,所以所有函数的_proto_ 都指向Function() 的原型对象
  • 所有的原型对象(包括Function 的原型对象) 都是Object 的实例,所以_proto_都指向Object(构造函数) 的原型对象。 而Object 构造函数的 _proto_ 指向null
  • Function 构造函数本身就是Function的实例,所以_proto_ 指向Function的原型对象

9. 对象继承方法

继承

继承的核心思想就是,能够继承父类方法的同时,保证自己的私有属性和方法。

四个最常用的继承方式

原型继承

  • 核心思想:将父类的实例作为子类的原型
  • 优点:方法复用,由于方法定义在父类的原型上,复用了父类构造函数原型上的方法
  • 缺点:
    • 创建的子类实例不能传参
    • 子类实例共享了父类构造函数的引用属性

组合继承

  • 核心思想:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用
  • 优点:
    • 可传参:子类实例创建可以传递参数
    • 方法复用:同时所有的子类可以复用父类引用类型的共享
  • 缺点:
    • 组合继承调用了两次父类的构造函数,造成了不必要的消耗

寄生组合继承

  • 核心思想:组合继承+原型继承结合两者的优点
  • 优点:完美!
  • 缺点:无!
// 父类 
function Father(name){
      
    this.name = name;
    this.colors = ["red","blue","green"]; 
}
// 方法定义在原型对象上(共享) 
Father.prototype.sayName = function(){
      
    alert(this.name); 
};
function Son(name,age){
      
    Father.call(this,name); // 核心 
    this.age = age; 
}
Son.prototype = Object.create(Father.prototype); // 核心: 
Son.prototype.constructor = Son; // 修复子类的 constructor 的指向

ES6的 extend 继承

ES6 的extend 继承其实就是寄生组合式继承的语法糖。

  • 核心思想:

    • extends: 内部相当于设置了Son.prototype = Object.create(Father.prototype);
    • super() :内部相当于调用了Father.call(this)
  • 小结:

    • 子类只要继承父类,可以不写constructor, 一旦写了,则在constructor 中的第一句话必须是super.
    • 把父类当作普通方法执行,给方法传递参数,让方法中的this 是子类的实例
    class Son extends Father {
            // Son.prototype.__proto__ = Father.prototype
        constructor(y) {
            
            super(200); // super(200) => Father.call(this,200) 
            this.y = y } }
    

10. 深浅拷贝

什么是深浅拷贝?

深浅拷贝是只针对Object和Array这样的引用数据类型的

  • 浅拷贝:只进行一层关系的拷贝,如果属性是基本类型,直接拷贝基本类型的值,如果属性值是内存地址,就拷贝这个地址,新旧对象公用一块内存
  • 深拷贝:进行无限层次的拷贝,会创造一个一摸一样的对象,不共享内存,修改对象不会互相影响

为什么要进行深浅拷贝?

let arr1 = arr2 = [1,2,3]
let obj1 = obj2 = {
     a:1, b:2, c:3}
arr1[0] = 2
obj1.a = 2
console.log(arr2[0]) // 2
console.log(obj2.a) // 2

从上面的代码可以看出:同一个Array或者Object赋值给两个不同变量时,变量指向的是同一个内存地址,改变其中一个变量的属性值,另一个也会改变。如果我们想要的是两个初始值相等但互不影响的变量,就要使用到拷贝。

深浅拷贝的使用

浅拷贝:

  • 扩展运算符(ES6新语法)

    let a = {
           c: 1}
    let b = {
           ...a}
    a.c = 2
    console.log(b.c) // 1
    
  • Object.assign(target, source)

    将source的值浅拷贝到target目标对象上

    let a = {
           c: 1}
    let b = Object.assign({
           }, a)
    a.c = 2
    console.log(b.c) // 1
    

深拷贝:

  • JSON.stringify()

    let obj = {
           
        name: 'lxy',
        city: {
           
            city1: '北京',
            city2: '上海'
        }
    }
    // 浅拷贝
    let obj1 = {
           ...obj}
    // 深拷贝
    let obj2 = JSON.stringify(obj)
    // 改变源对象的引用类型值
    obj.city.city1 = '杭州'
    console.log(obj1.city.city1) // 杭州
    console.log(JSON.parse(obj2).city.city1) // 北京
    

深浅拷贝的手动实现?

浅拷贝:

循环遍历对象,将对象的属性值拷贝到另一个对象中,返回该对象。

function shallowClone(o) {
     
    const onj = {
     };
    for(let i in o) {
     
        obj[i] = o[i]
    }
    return obj;
}

深拷贝:(简单实现)

对于深拷贝来说,就是在浅拷贝的基础上加上递归

var a1 = {
     
    b: {
     
        c: {
     
            d: 1
        }
    }
}
function deepClone(obj) {
     
    var target = {
     }
    for(var i in obj) {
     
        if(obj.hasOwnProperty(i)) {
     
            if(typeof obj[i] === 'object') {
     
                target[i] = deepClone(obj[i])
            } else {
     
                target[i] = obj[i]
            }
        }
    }
    return target
}

11. js事件循环机制

JavaScript是一门单线程非阻塞的脚本语言。

  • 单线程:代码执行时,都只有一个主线程来处理所有的任务,
  • 非阻塞:是指进行异步任务时,主线程会挂起这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。JavaScript引擎是通过 event loop (事件循环) 实现非阻塞的

主线程从任务队列读取事件,这个过程是循环不断地,所以整个运行机制又称为Event Loop(事件循环)

执行上下文

执行上下文是一个抽象的概念,可以理解为是代码执行的一个环境。分为全局执行上下文,函数(局部)执行上下文,Eval执行上下文

  • 全局执行上下文: this指向的是window,标签中的代码
  • 函数执行上下文:每个函数调用的时候,会创建一个新的函数执行上下文
  • Eval执行上下文:不常用

执行栈

  • “栈”,一种数据结构。具有“先进后出”的特点。
  • 代码执行的时候,遇到一个执行上下文就将其依次压入执行栈中。
  • 先执行位于栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕就会出栈,继续执行下一个位于栈顶的执行上下文。
function foo() {
     
  console.log('a');
  bar();
  console.log('b');
}
function bar() {
     
  console.log('c')
}
foo()

代码解释:

  • 初始化状态,执行栈为空
  • foo(), foo函数执行,foo进入执行栈,console.log(‘a’),打印a
  • bar(), 执行函数bar,bar 进入执行栈,开始执行bar函数,console.log(‘c’) , 打印c
  • bar函数执行完毕,出栈,继续执行foo函数
  • console.log(‘b’) , 打印b, foo函数执行完毕。出栈。

宏任务

宏任务一般包括:

  • 整体的script标签内的代码
  • setTimeout
  • setInterval
  • setImmediate(Node)
  • I/o

微任务

微任务一般包括:

  • Promise
  • process.nextTick(Node)–所有异步任务之前触发(nextTick 队列会比 Promie 队列先执行。)
  • MutationObserver

循环机制的运行

  • 首先,事件循环机制是从

你可能感兴趣的:(前端面试,javascript)