Javascript 面试题助你拿到offer
Javascript是前端面试的重点,本文重点梳理下 Javascript 中的常考知识点,本文只罗列了一些重难点。
1. JavaScript 有哪些数据类型
6种原始数据类型:
- Boolean: 布尔表示一个逻辑实体,可以有两个值:true 和 false
- Number: 用于表示数字类型
- String: 用于表示文本数据
- Null: Null 类型只有一个值: null,特指对象的值未设置
- Undefined: 一个没有被赋值的变量会有个默认值 undefined
- Symbol: 符号(Symbols)是ECMAScript第6版新定义的。符号类型是唯一的并且是不可修改的
引用类型:
- 统称为Object对象,主要包括对象、数组和函数。
基本类型和引用类型的区别:
基本类型和引用类型存储于内存的位置不同,基本类型直接存储在栈中,而引用类型的对象存储在堆中,与此同时,在栈中存储了指针,而这个指针指向正是堆中实体的起始位置。下面通过一个小题目,来看下两者的主要区别:
// 基本类型
var a = 10
var b = a
b = 20
console.log(a) // 10
console.log(b) // 20
上述代码中,a b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:
// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}
上述代码中,a b都是引用类型。在执行了b = a之后,修改b的属性值,a的也跟着变化。因为a和b都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b修改属性时,a的值随之改动
2.怎么判断不同的JS数据类型
typeof
返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型
null、array返回Object
typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 对象
typeof [] ; //object 对象
typeof new Date(); //object 对象
typeof new RegExp(); //object 对象
instanceof
instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测null 和 undefined
let a = [];
a instanceof Array // true
a instanceof Object // true
null instanceof Null//报错
undefined instanceof undefined//报错
constructor
constructor作用和instanceof非常相似。但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。
不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。
function F() {};
var f = new F;
f.constructor == F // true
F.prototype = {a: 1}
var f = new F
f.constructor == F // false
toString
Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(11) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call([]) ; // [object Array]
3.undefined 和 null 有什么区别
null表示"没有对象",即该处不应该有值
典型用法:
- 作为函数的参数,表示该函数的参数不是对象
- 作为对象原型链的终点
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义
典型用法:
- 变量被声明了,但没有赋值时,就等于
undefined - 调用函数时,应该提供的参数没有提供,该参数等于
undefined - 对象没有赋值的属性,该属性的值为
undefined - 函数没有返回值时,默认返回
undefined
4.call/apply/bind它们有什么区别,手动实现
相同点:三者都可以改变 this 的指向
不同点:
- apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组
var obj = {
name : 'sss'
}
function func(firstName, lastName){
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
func.apply(obj, ['A', 'B']); // A sss B
- call 方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组
var obj = {
name: 'sss'
}
function func(firstName, lastName) {
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
func.call(obj, 'C', 'D'); // C sss D
- bind 接受的参数有两部分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数
var obj = {
name: 'sss'
}
function func() {
console.log(this.name);
}
var func1 = func.bind(null, 'xixi');
func1();
apply、call方法都会使函数立即执行,因此它们也可以用来调用函数bind方法不会立即执行,而是返回一个改变了上下文this后的函数。而原函数 fun 中的 this 并没有被改变,依旧指向全局对象windowbind在传递参数的时候会将自己带过去的参数排在原函数参数之前
function fun(a, b, c) {
console.log(a, b, c);
}
var fun1 = fun.bind(this, 'xixi');
fun1(1,2) // xixi 1 2
实现一个call函数
// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.mycall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let arg = [...arguments].slice(1)
let result = context.fn(...arg)
delete context.fn
return result
}
实现一个apply函数
// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myapply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let result
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
实现一个bind函数
// 思路:类似call,但返回的是函数
Function.prototype.mybind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let _this = this
let arg = [...arguments].slice(1)
return function F() {
// 处理函数使用new的情况
if (this instanceof F) {
return new _this(...arg, ...arguments)
} else {
return _this.apply(context, arg.concat(...arguments))
}
}
}
5.浅拷贝与深拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
浅拷贝的实现方式:
- Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
- Array.prototype.concat()
- Array.prototype.slice()
深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。
深拷贝的实现方式:
- 热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝
- JSON.parse(JSON.stringify(目标对象),缺点就是只能拷贝符合JSON数据标准类型的对象
- Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
手写递归方法:
递归实现深拷贝的原理:要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复。
var person = {
name: 'tt',
age: 18,
friends: ['oo', 'cc', 'yy']
}
function shallowCopy(source) {
if (!source || typeof source !== 'object') {
throw new Error('error');
}
var targetObj = source.constructor === Array ? [] : {};
for (var keys in source) {
if (source.hasOwnProperty(keys)) {
targetObj[keys] = source[keys];
}
}
return targetObj;
}
var p1 = shallowCopy(person);
console.log(p1)
数组类型: 这种最难,因为数组中的元素可能是基础类型、对象还可能数组,因此要专门做一个函数来处理数组的深拷贝
/**
* 数组的深拷贝函数
* @param {Array} src
* @param {Array} target
*/
function cloneArr(src, target) {
for (let item of src) {
if (Array.isArray(item)) {
target.push(cloneArr(item, []));
} else if (typeof item === "object") {
target.push(deepClone(item, {}));
} else {
target.push(item);
}
}
return target;
}
/**
* 对象的深拷贝实现
* @param {Object} src
* @param {Object} target
* @return {Object}
*/
function deepClone(src, target) {
const keys = Reflect.ownKeys(src);
let value = null;
for (let key of keys) {
value = src[key];
if (Array.isArray(value)) {
target[key] = cloneArr(value, []);
} else if (typeof value === "object") {
// 如果是对象而且不是数组, 那么递归调用深拷贝
target[key] = deepClone(value, {});
} else {
target[key] = value;
}
}
return target;
}
6.原型和原型链
首先明确一点,JavaScript是基于原型的
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针.

- 每一个构造函数都拥有一个prototype属性,这个属性指向一个对象,也就是原型对象
- 原型对象默认拥有一个constructor属性,指向指向它的那个构造函数
- 每个对象都拥有一个隐藏的属性[[prototype]],指向它的原型对象
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找。如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么 —— Object.prototype.proto === null
7. JavaScript 如何实现继承
- 原型链继承
function Animal() {}
Animal.prototype.name = 'cat'
Animal.prototype.age = 1
Animal.prototype.say = function() {console.log('hello')}
var cat = new Animal()
cat.name // cat
cat.age // 1
cat.say() // hello
最简单的继承实现方式,但是也有其缺点
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
- 要想为子类新增属性和方法,必须要在new语句之后执行,不能放到构造器中
- 构造继承
function Animal() {
this.species = "动物"
}
function Cat(name, age) {
Animal.call(this)
this.name = name
this.age = age
}
var cat = new Cat('豆豆', 2)
cat.name // 豆豆
cat.age // 2
cat.species // 动物
使用call或apply方法,将父对象的构造函数绑定在子对象上.
- 组合继承
function Animal() {
this.species = "动物"
}
function Cat(name){
Animal.call(this)
this.name = name
}
Cat.prototype = new Animal() // 重写原型
Cat.prototype.constructor = Cat
如果没有Cat.prototype = new Animal()这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal.这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat
- extends 继承 ES6新增继承方式,Class 可以通过extends关键字实现继承
class Animal {
}
class Cat extends Animal {
constructor() {
super();
}
}
使用 extends 实现继承,必须添加 super 关键字定义子类的 constructor,这里的super() 就相当于 Animal.prototype.constructor.call(this)
当然,还有很多种实现继承的方式,这里就不多说了。然后,再推荐一波 红宝书
8.同步 vs 异步
- 同步,我的理解是一种线性执行的方式,执行的流程不能跨越。比如说话后在吃饭,吃完饭后在看手机,必须等待上一件事完了,才执行后面的事情。
- 异步,是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式。在程序中异步处理的结果通常使用回调函数来处理结果。
// 同步
console.log(100)
alert(200);
console.log(300) //100 200 300
// 异步
console.log(100)
setTimeout(function(){
console.log(200)
})
console.log(300) //100 300 200
异步和单线程
JS 需要异步的根本原因是 JS 是单线程运行的,即在同一时间只能做一件事,不能“一心二用”。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
一个 Ajax 请求由于网络比较慢,请求需要 5 秒钟。如果是同步,这 5 秒钟页面就卡死在这里啥也干不了了。异步的话,就好很多了,5 秒等待就等待了,其他事情不耽误做,至于那 5 秒钟等待是网速太慢,不是因为 JS 的原因。
最后
后面会在持续更新,欢迎关注点个赞!
