JavaScript-面试集锦

JS有哪些基本数据类型

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol(ES6新定义,Symbol生成一个全局唯一、表示独一无二的值)
  • Object(Array,Function,Object)
    对象是引用类型,在使用过程会遇到深拷贝和浅拷贝的问题
    1
    2
    3
    4
    5
    let a = {name: 'whh', age: 18};
    let b = a;
    b.name = 'lsd';
    console.log(a); //{name:'lsd',age:18}
    console.log(b); //{name:'lsd',age:18}

关于闭包

闭包的定义: 在一个函数里面嵌套另一个函数 被嵌套的那个函数的作用域是一个闭包。

1
2
3
4
5
6
function a() {
let a = 1;
function b() {
console.log(a)
}
}

闭包的用途:

  1. 读取函数内部的变量。
  2. 让这些变量的值始终保存在内存中。
    使用闭包的注意点:
  3. 由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题,在IE中可能导致内存泄漏。解决方法是,在退出函数之前,将不适用的局部变量全部删除。
  4. 闭包会在父函数外部改变父函数内部变量的值。不要随便更改父函数内部变量的值。
    经典面试题:循环中使用闭包解决var定义函数的问题
    1
    2
    3
    4
    5
    for(var i=1; i<=5; i++) {
    setTimeout(function timer() {
    console.log(i);
    }, i*1000);
    }

首先因为setTimeout是个异步函数,所以会先把所有循环执行完毕,这时候i就是6了,所以会输出一堆6。有三种解决办法:

  1. 使用闭包

    1
    2
    3
    4
    5
    6
    7
    for(var i=1;i<=5;i++) {
    (function(j) {
    setTimeout(function timer() {
    console.log(j);
    }, j*1000);
    })(i);
    }
  2. 使用setTimeout的第三个参数

    1
    2
    3
    4
    5
    for(var i=1;i<=5;i++) {
    setTimeout(function timer(j) {
    console.log(j);
    },i*1000,i);
    }
  3. 使用let定义i

    1
    2
    3
    4
    5
    for(let i=1;i<=5;i++) {
    setTimeout(function timer() {
    console.log(i);
    },i*1000);
    }

因为let会创建一个块级作用域,详细解释请阅读文章ES6手册

this的指向

在JS中,this是指正在执行函数的”所有者”,或者更确切地说,指将当前函数作为方法的对象。

  1. 当以方法的形式被调用时(如person.fullname()): this指向父级对象。
  2. 否则: 严格模式下为undefined 非严格模式下为window(最外层对象)。
  3. 当作为构造器被调用时,this指代即将生成的对象。
  4. 箭头函数中的this取决于它外面的第一个不是箭头函数的this。

    call,apply,bind的区别

    都是用于绑定this的指向 call和apply不同点在于apply接收一个数组传参(除了第一个传参) 视情况选择 立即执行
    bind与call,apply不同 返回一个新函数 而不立即执行。
    bind()方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var whh = {
    name: '王花花',
    }
    var lsd = {
    name: '李拴蛋',
    }
    yo.call(whh,'赵可爽',1,2,3); //直接执行
    yo.apply(whh,['赵可爽',1,2,3]); //直接执行
    var yo2 = yo.bind(whh); //将绑定了新环境的function返回等待被调用
    yo2('赵可爽');

for和for…in的区别

  • 遍历数组时的异同: for循环,数组下标的typeof类型为number,for…in循环数组下标的typeof类型为string。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arr = ['whh','lsd','aks','lbb'];
    for(let i=0; i<arr.length; ++i) {
    console.log(typeof i); //number
    }

    let arr = ['whh','lsd','aks','lbb'];
    for(let it in arr) {
    console.log(typeof it); //string
    }
  • 遍历对象时的异同: for循环无法遍历对象,获取不到obj.length。for…in循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组成的数组。

深浅拷贝

深浅拷贝都是针对引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型复制,则会进行地质的拷贝,最终两个变量指向同一份数据。

1
2
3
4
5
6
let a = {
age: 18,
};
let b = a;
a.age = 19;
console.log(b.age);

如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。通常在开发中我们不希望出现这样的问题,我们可以使用 浅拷贝 来解决这个问题。

  1. 浅拷贝:
  • 通过Object.assign

    1
    2
    3
    4
    5
    6
    let a = {
    age: 18,
    };
    let b = Object.assign({},a);
    b.age = 20;
    console.log(a.age); //18
  • 通过展开运算符…

    1
    2
    3
    4
    5
    6
    let a = {
    age: 18,
    };
    let b = {...a};
    a.age = 20;
    console.log(b.age); //18
  • 通过遍历对象属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function clone(obj) {
    let target = {};
    for(var i in obj) {
    if(obj.hasOwnProperty(i)) {
    target[i] = source[i];
    }
    }
    return target;
    }
  1. 深拷贝:
    浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么两者享有相同的引用。要解决这个问题,需要引入深拷贝。
    这个问题通常通过JSON.parse(JSON.stringify(obj))来解决
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    name: 'whh',
    age: 'lsd',
    address: {
    country: 'China',
    province: 'Guangdong',
    }
    }
    let new_obj = JSON.parse(JSON.stringify(obj));
    new_obj.address.province = 'Fujian';
    console.log(obj.address.province); //'Guangdong'

此方法的局限性: 会忽略undefined / 不能序列化函数 / 不能解决循环引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj);

如果有这样一个循环引用对象,我们就不能用上面的办法进行深拷贝。
在遇到函数或者undefined的时候,该对象也不能正常的序列化。

1
2
3
4
5
6
7
let a = {
age: undefined,
jobs: function() {},
name: 'link',
}
let b = JSON.parse(JSON.stringify(a));
console.log(b); // {name: 'link'}

在上述情况中,我们会发现,该方法会忽略掉函数和undefined。我们可以使用lodash的深拷贝函数
如果所需拷贝的对象中含有内置类型并且不包含函数,可以使用MessageChannel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function structuralClone() {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2 = ev => resolve(ev.data);
port1.postMessage(obj);
})
}
let obj = {
a: 1,
c: {
c: b,
}
}
//该方法是异步的
//可以处理undefined和循环引用的对象
const clone = structuralClone(obj);
console.log(clone);

typeof

typeof对于基本类型,除了null都可以显示正确的类型。

1
2
3
4
5
6
7
8
typeof '1'; //'string'
typeof 1; //'number'
typeof undefined; //'undefined'
typeof true; // 'boolean'
typeof Symbol(); //'symbol'
typeof b; //'undefined' b未声明所以显示'undefined'
但是对于null来说,虽然它是基本类型,但它会显示object,这是JS中存在已久的bug。
typeof null; // 'object'

typeof对于对象,除了函数都会显示object。

1
2
3
typeof []; //'object'
typeof {}; //'object'
typeof console.log() //'function'

所以如果我们想获得一个变量的正确类型,可以使用Object.prototype.toString.call(obj)。这样我们就可以获得一个类似[object Type]的字符串。

1
Object.prototype.toString.call({}); //[object Object]

四则运算符

只有当加法运算时,其中一个值为字符串类型,就会把另一个也转换为字符串类型。在其他运算中,只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换: 将值转换为原始值、转换为数字、转换为字符串。
对于加号要注意这个表达式: 'a'++'b' -> 'aNaN'
因为+'b'为NaN。

如何检查一个数字是否为整数?

检查一个数字是整数还是小数,可以将它对1进行取模,看看是否有余数。

1
2
3
4
5
function isInt(num) {
return num % 1 === 0;
}
console.log(isInt(0.1)); //false;
console.log(isInt(8)); //true;

原型

每个函数都有prototype属性,除了Function.prototype.bind(),该属性指向原型。
每个对象都有__proto_属性,指向了创建该对象的构造函数。实际它指向的是该构造函数的[[prototype]],但是[[prototype]]是内部属性,我们并不能访问到,所以使用__proto_来访问。
对象可以通过__proto_来访问不属于该对象的属性,__proto将对象连接起来组成原型链

JS事件机制

DOM事件流存在三个阶段: 事件捕获、处于目标、事件冒泡。
事件捕获: 当鼠标点击或者触发DOM事件时,浏览器会从根节点开始由外到内进行事件传播,也就是说,当我们点击了子元素,父元素通过事件捕获的方式注册了事件,那么就会先触发父元素绑定的事件。
事件冒泡: 与事件捕获相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
DOM标准事件流触发的先后顺序为: 先捕获再冒泡,即当触发DOM事件时,会先进行事件捕获,捕获到事件源后通过事件传播进行事件冒泡。
事件代理: 如果一个节点中的子节点是动态生成的,并且它的子节点需要注册事件,那么就应该将事件注册在父节点上。
事件代理的优点:

  • 节省内存
  • 不需要给子节点注销时间

Event loop 事件循环

JS是门非阻塞单线程语言,在执行过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中,如果遇到异步的代码,会被挂起并加入到Task队列中(有多种task)。一旦执行栈为空,Event loop就会从Task队列中拿出需要执行的代码并放到执行栈中执行。
不同的任务源会被分配到不同的Task队列中,任务源可以分为微任务(microtask)宏任务(macrotask)。在ES6规范中,microtask称为jobs,macrotask称为task。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
},0);
new Promise((resolve) => {
console.log('Promise');
resolve();
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
//script start => Promise => script end => promise1 => promise2 => setTimeout

微任务包括process.nextTick、promise、Object.observe、MutationObserver
宏任务包括script、setInterval、setTimeout、setImmediate、I/O、UI rendering

一次正确的Event loop顺序

  1. 执行同步代码 这属于宏任务。
  2. 执行栈为空 查询是否有微任务需要进行。
  3. 执行所有微任务。
  4. 必要的话渲染UI。
  5. 然后开始下一轮Event loop,执行宏任务中的异步代码。

项目上线前的性能优化

  • 图片预加载,css样式表放在顶部且link链式引入,javascript放在底部body结束标签前。
  • 减少http请求,图片静态资源放在cdn托管。
  • 减少DOM操作次数,优化javascript性能。
  • 减少DOM元素数量,合理利用:after、:before等伪类。
  • 使用dns-prefetch对项目中用到的域名进行DNS预解析,减少 DNS 查询,如:<link rel=”dns-prefetch” href=”//github.com”/>;
  • JS、CSS压缩 合并
  • 图片压缩 合并(图片的合并可以使用CSS Spirite 方法就是把一些小图用ps合成一张图,用CSS定位显示每张图片的位置)
    gulp图片压缩代码:
    1
    2
    3
    4
    5
    6
    //压缩image
    gulp.task('imagemin', function () {
    gulp.src('./public/**/*.{png,jpg,gif,ico,jpeg}')
    .pipe(imagemin())
    .pipe(gulp.dest('./public'));
    });
0%