Javascript 面试题(基础)

419 阅读10分钟

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

返回一个表示数据类型的字符串,返回结果包括:numberbooleanstringsymbolobject、undefinedfunction等7种数据类型

nullarray返回 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,否则返回falseinstanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测nullundefined

let a = [];
a instanceof Array  // true
a instanceof Object // true
null instanceof Null//报错
undefined instanceof undefined//报错

constructor

constructor作用和instanceof非常相似。但constructor检测 Objectinstanceof不一样,还可以处理基本数据类型的检测。 不过函数的 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表示"没有对象",即该处不应该有值

典型用法:

  1. 作为函数的参数,表示该函数的参数不是对象
  2. 作为对象原型链的终点

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义

典型用法:

  1. 变量被声明了,但没有赋值时,就等于undefined
  2. 调用函数时,应该提供的参数没有提供,该参数等于undefined
  3. 对象没有赋值的属性,该属性的值为undefined
  4. 函数没有返回值时,默认返回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();

applycall 方法都会使函数立即执行,因此它们也可以用来调用函数 bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 fun 中的 this 并没有被改变,依旧指向全局对象 window bind 在传递参数的时候会将自己带过去的参数排在原函数参数之前

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.浅拷贝与深拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

浅拷贝的实现方式:

  1. Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
  2. Array.prototype.concat()
  3. Array.prototype.slice()

深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

深拷贝的实现方式:

  1. 热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝
  2. JSON.parse(JSON.stringify(目标对象),缺点就是只能拷贝符合JSON数据标准类型的对象
  3. 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

最简单的继承实现方式,但是也有其缺点

  1. 来自原型对象的所有属性被所有实例共享
  2. 创建子类实例时,无法向父类构造函数传参
  3. 要想为子类新增属性和方法,必须要在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 的原因。

最后

后面会在持续更新,欢迎关注点个赞!

参考