分享一下遇到过普遍的面试题(上篇-js基础)

78 阅读16分钟

为大家整理了一下面试经常遇到的一些面试题(js基础篇)

1、解释一下什么是回调函数,并提供一个简单的例子?

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、 回调和异步调用;

同步调用是一种阻塞式调用,调用方要等待对方执行完毕才 返回,它是一种单向调用;

回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;

异步调用是一种类似消息或事件的机制,不过它的 调用方向刚好相反,接口的服务在收

到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用

的关系非常紧密,通常我们使用回 调来实现异步消息的注册,通过异步调用来实现消息的通

知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨

论回调机制在 不同软件架构中的实现

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递

给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数

不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于

对该事件或条件进行响应

案例:

#include<stdio.h>
//callbackTest.c
//1.定义函数 onHeight(回调函数)
//@onHeight 函数名
//@height 参数
//@contex 上下文
void onHeight(double height, void *contex) {
 printf("current height is %lf", height);
}

//2.定义 onHeight 函数的原型
//@CallbackFun 指向函数的指针类型
//@height 回调参数,当有多个参数时,可以定义一个结构体
//@contex 回调上下文,在 C 中一般传入 nullptr,在 C++中可传入对象指针
typedef void (*CallbackFun)(double height, void *contex);
//定义全局指针变量
CallbackFun m_pCallback;
//定义注册回调函数
void registHeightCallback(CallbackFun callback, void *contex) {
    m_pCallback = callback;
}

//定义调用函数
void printHeightFun(double height) {
    m_pCallback(height, NULL);
}

//main 函数
int main() {
    //注册回调函数 onHeight
    registHeightCallback(onHeight, NULL);
    //打印 height
    double h = 99;
    printHeightFun(99);
}

2、什么是闭包?

闭包就是能够读取其他函数内部变量的函数。例如在 javascript 中,只有函数内部的子 函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭 包是将函数内部和函数外部连接起来的桥梁。

举例:创建闭包最常见方式,就是在一个函数内部创建另一个函数。下面例子中的 closure 就是一个闭包

function func(){
var a =1 ,b = 2;
funciton closure(){ return a+b; } 
    return closure;
}

3、什么是内存泄漏?哪些操作会造成内存泄漏?如何解决?

什么是内存泄漏: 内存泄漏指任何对象在您不再拥有或需要它之后仍然存在

*那些操作会造成:

  • 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象 的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对 象的内存即可回收
  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏
  • 闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)

*如何解决:

  • global variables(全局变量):对未声明的变量的引用在全局对象内创建一个新变量。在浏览器中, 全局对象就是 window。
function foo(arg) {
    bar = 'some text'; // 等同于 window.bar = 'some text';
}

function foo() {
    this.var1 = 'potential accident'
}
  • 可以在 JavaScript 文件开头添加 “use strict”,使用严格模式。这样在严格模式下解析 JavaScript 可以防止意外的全局变量
  • 在使用完之后,对其赋值为 null 或者重新分配
  • 被忘记的 Timers 或者 callbacks在 JavaScript 中使用 setInterval 非常常见 大多数库都会提供观察者或者其它工具来处理回调函数,在他们自己的实例变为不可达 时,会让回调函数也变为不可达的。对于 setInterval,下面这样的代码是非常常见的
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.
这个例子阐述着 timers 可能发生的情况:计时器会引用不再需要的节点或数据
  • 闭包:一个可以访问外部(封闭)函数变量的内部函数

    JavaScript 开发的一个关键方面就是闭包:一个可以访问外部(封闭)函数变量的内部函数。由于 JavaScript 运行时的实现细节,可以通过以下方式泄漏内存

var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        if (originalThing) // a reference to 'originalThing'
            console.log("hi");
        };
        theThing = {
            longStr: new Array(1000000).join('*'),
            someMethod: function () {
            console.log("message");
        }
    };
};
setInterval(replaceThing, 1000);

4、说说你对原型(prototype)理解

JavaScript 是一种通过原型实现继承的语言与别的高级语言是有区别的,像 java,C#是通 过类型决定继承关系的,JavaScript 是的动态的弱类型语言,总之可以认为 JavaScript 中所 有都是对象,在 JavaScript 中,原型也是一个对象,通过原型可以实现对象的属性继承, JavaScript 的对象中都包含了一个” prototype”内部属性,这个属性所对应的就是该对象 的原型; “prototype”作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象 的原型,Firefox 和 Chrome 内核的 JavaScript 引擎中提供了”proto“这个非标准的访问器 (ECMA 新标准中引入了标准对象原型访问器”Object.getPrototype(object)”) 原型的主要作用就是为了实现继承与扩展对象

5、介绍下原型链(解决的是继承问题吗)

  • JavaScript 原型:每个对象都会在其内部初始化一个属性,就是 prototype(原型)

原型链: 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype,于是就这样一直找 下去,也就是我们平时所说的原型链的概念

特点: JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己 的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变

6、介绍 this 各种情况

  • 以函数形式调用时,this 永远都是 window;
  • 以方法的形式调用时,this 是调用方法的对象;
  • 以构造函数的形式调用时,this 是新创建的那个对象;
  • 使用 call 和 apply 调用时,this 是指定的那个对象;
  • 箭头函数:箭头函数的 this 看外层是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,就是 window;
  • 特殊情况:通常意义上 this 指针指向为最后调用它的对象。这里需要注意的一点就是 如果返回值是一个对象,那么 this 指向的就是那个返回的对象,如果返回值不是一个对象那么 this 还是指向函数的实例;

7、数组中的 forEach 和 map 的区别?

相同点:

  • 都是循环遍历数组中的每一项;
  • forEach 和 map 方法里每次执行匿名函数都支持 3 个参数,参数分别是 item(当前每一项), index(索引值),arr(原数组)

不同点:

  • map 方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值
  • map 方法不会对空数组进行检测,map 方法不会改变原始数组(有些情况下会改变,当遍历数据是引用数据类型时)
  • forEach 方法用来调用数组的每个元素,将元素传给回调函数
  • forEach 对于空数组是不会调用回调函数的。 无论 arr 是不是空数组,forEach 返回的都是 undefined。这个方法只是将数组中的每一项作为 callback 的参数执行一次

8、Call 和 apply,bind 的区别

call() 与apply()只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。 call(); 传递参数类型 直接返回调用结果(都可以改变this指向)

function fun(name, age) { 
    this.name = name; 
    this.age = age; 
} 
function allFun(name, age) { 
    fun.call(this, name, age); 
    this.grade = '3班'; 
} 
console.log(new allFun('cheese', 5).name);

apply() 传递具体参数类型,第二个参数传数组

var num = [1,3,12,11,54]; 
Math.max.apply(this,num);
console.log(Math.max.apply(null,num)); //使用null 也可以第一个参数 

bind()

bind()方法创建一个新的函数,当这个新的函数被调用时,其 this 置为提供的值,其参数 列表前几项,置为创建时指定的参数序列

function title(){ 
    setInterval(function(){ 
        console.log('hello world'); 
     },1000); 
 } 
 title.bind(this)() //加上调用的括号就可以执行了。

9、New 操作符具体干了什么呢?

  • 创建一个空对象: 并且 this 变量引入该对象,同时还继承了函数的原型;
  • 设置原型链 空对象指向构造函数的原型对象;
  • 执行函数体 修改构造函数 this 指针指向空对象,并执行函数体;
  • 判断返回值 返回对象就用该对象,没有的话就创建一个对象;

10、用 JavaScript 实现排序

// 一般写法
function sort(arr){
    for (var i = 0; i <arr.length; i++) {
        for (var j = 0; j <arr.length-i; j++) {
            if(arr[j]>arr[j+1]){
                var c=arr[j];//交换两个变量的位置
                arr[j]=arr[j+1];
                arr[j+1]=c;
            }
        };
    };
    return arr.toString();
}
console.log(sort([23,45,18,37,92,13,24]));
// 快排 1
function inserSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        let j = i;
        let target = arr[j]
        weile (j > 0 && arr[j - 1] > target) {
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = target;
    }
    return arr;
}
console.log(insersort([2,3,3,6,1,9]))
// 快排 2
function quickSort(arr) {
    if (arr.length < 2) { return arr; }
    let cur = arr[arr.length - 1;]
    let left = arr.filter((v, i) => {
            if (v <= cur && i !== arr.length - 1) { return v;}
        })
    let right = arr.filter(v => {
            if (v > cur) { return v; }
        })
    return [...quickSort(left), cur, ...quickSort(right)];
}

11、Split()和 join()的区别?

  • Split()是把一串字符(根据某个分隔符)分成若干个元素存放在一个数组里,即切割成数组的形式;
  • join() 是把数组中的字符串连成一个长串,可以大体上认为是 Split()的逆操作

12、JavaScript 中如何对一个对象进行深度 clone?

function clone(obj){
    if (typeof obj === 'object') {
        if (obj instanceof Array) {
            var result=[];
            for(let i = 0; i < obj.length; i++) {
                result[i] = clone(obj[i]);
            }
            return result;
        } else {
            let result = {};
            for(var i in obj) {
                result[i] = clone(obj[i]);
            }
            return result;
        }
    } else {
        return obj;
    }
}
let obj1=[12, {a: 11, b: 22}, 5];
let obj2=clone(obj1);
obj2[1].a+=5;
console.log(obj1, obj2);

13、js 数组去重,能用几种方法实现

  • 使用 es6 set 方法 [...new Set(arr)]
  • 利用新数组 indexOf 查找 indexOf() 方法可返回某个指定的元素在数组中首次出现的位置。如果没有就返回-1
  • 利用 Map 数据结构去重
  • 利用 filter

代码这里就不在实现了,很基础

14、谈谈你对 Javascript 垃圾回收机制的理解?

  • 标记清除(mark and sweep):

    这是 JavaScript 最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了

  • 引用计数(reference counting)

    在低版本 IE 中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个 变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用空间

    在 IE 中虽然 JavaScript 对象通过标记清除的方式进行垃圾回收,但 BOM 与 DOM 对象却是通过引用计数回收垃圾的,也就是说只要涉及 BOM 及 DOM 就会出现循环引用问题

15、js 如何处理防抖和节流

例如:在进行窗口的 resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕此时我们可以采用 debounce(防抖)和 throttle(节流)的方式来减少调用频率,同时又不影响实际效果。

  • 函数防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数 才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时如下,持续触发 scroll 事件时,并不执行 handle 函数,当 1000 毫秒内没有触发 scroll 事件时, 才会延时触发 scroll 事件
function debounce(fn, wait) {
    var timeout = null;
    return function() {
        if(timeout !== null) clearTimeout(timeout);
        timeout = setTimeout(fn, wait);
    }
} 
// 处理函数 
function handle() {
    console.log(Math.random());
}
// 滚动事件 
window.addEventListener('scroll', debounce(handle, 1000));
  • 函数节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴 ;

例如:持续触发 scroll 事件时,并不立即执行 handle 函数,每隔 1000 毫秒才会执行一次handle函数

var throttle = function(func, delay) {
    var prev = Date.now();
    return function() {
        var context = this;
        var args = arguments;
        var now = Date.now();
        if (now - prev >= delay) {
            func.apply(context, args);
            prev = Date.now();
        }
    }
}
function handle() {console.log(Math.random());}
window.addEventListener('scroll', throttle(handle, 1000));

总结:函数防抖将几次操作合并为一此操作进行。原理是维护一个计时器,规定在 delay 时间后 触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一 来,只有最后一次操作能被触发函数节流使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

区别:函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现

16 Eval 是做什么的?

  • eval()的作用把字符串参数解析成 JS 代码并运行,并返回执行的结果
    eval("2+3");//执行加运算,并返回运算值
    eval("varage=10");//声明一个 age 变量
  • eval 的作用域在它所有的范围内容有效
function a(){
    eval("var x=1"); //等效于 var x=1;
    console.log(x); //输出 1
}
a();
console.log(x);//错误 x 没有定义
  • JSON 字符串转换为 JSON 对象的时候可以用 eval
var json = "{name:'Mr.CAO',age:30}";
var jsonObj = eval("("+json+")");
console.log(jsonObj);

17 什么是进程、什么是线程、它们之间是什么关系(了解,js是单线程+同步异步)

进程

  • 程序执行时的一个实例
  • 每个进程都有独立的内存地址空间
  • 系统进行资源分配和调度的基本单位
  • 进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例
  • 进程里的方法区,是用来存放进程中的代码片段的,是线程共享的
  • 在多线程 OS 中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码

线程

  • 进程中的一个实体
  • 进程的一个执行路径
  • CPU 调度和分派的基本单位
  • 线程本身是不会独立存在
  • 当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行
  • 系统不会为线程分配内存,线程组之间只能共享所属进程的资源
  • 线程只拥有在运行中必不可少的资源(如程序计数器、栈)
  • 线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
  • 每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问

关系

  • 一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源
  • Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程
  • 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域

18什么是任务队列?

  • 宏任务(macrotask)在新标准中叫 task:主要包括:script(整体代码),setTimeout,setInterval setImmediate,I/O
  • 微任务(microtask)在新标准中叫 jobs:主要包括:process.nextTick, Promise,MutationObserver(html5 新特性)
  • 同步任务:在主线程上,排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务:不进入主线程,而进入“任务队列”(异步队列)的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行