前端面试题之JavaScript

ES6语法有哪些,分别怎么用

参考链接:http://es6.ruanyifeng.com/


new的执行过程

  1. 创建一个空对象;
  2. 将构造函数的 prototype 属性赋值给新对象的 __proto__ 属性,并将构造函数的 this 指向新对象;
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 将新对象返回;
function Student(name) {
  this.name = name
}

let studentA = new Student('a')
console.log(studentA)

// 等价于

let studentB = (function() {
  let obj = {}
  obj.__proto__ = Student.prototype
  Student.call(obj, 'b')
  return obj
})()
console.log(studentB)

异常捕获

如何优雅处理前端异常

错误处理机制


call、apply、bind区别

call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

fun.call(thisArg, arg1, arg2, ...)

apply

apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。当不确定参数的个数时,就可以使用 apply。

func.apply(thisArg, [argsArray])

bind

bind() 方法会返回一个新的函数,在 bind() 被调用时,这个新函数会 call 原来的函数,新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

function.bind(thisArg[,arg1[,arg2[, ...]]])

总结

其实 apply 和 call 基本类似,他们的区别只是传入的参数不同。call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
bind 和 call、apply 方法作用是一致的,只是该方法会返回一个新函数,并且我们必须要手动去调用。


什么是JSONP

它的基本思想是,网页通过添加一个 <script> 元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

请求方:xxx.com的前端程序员(浏览器)
响应方:yyy.com的后端程序员(服务器)

  1. 请求方创建 script,src 指向响应方,同时传一个查询参数 ?callback=xxx
  2. 响应方根据查询参数 callback,构造形如这样的响应:
      1. xxx.call(undefined,’需要的数据’)
      2. xxx(‘需要的数据’)
  3. 浏览器接收到响应,就会执行 xxx.call(undefined,’需要的数据’)
  4. 那请求方就知道了所需要的数据了

为什么不支持POST?

  1. JSONP 是通过动态创建 script 的
  2. 动态创建 script 时只能用 GET,没法用 POST

代码实现 zhouwanwen.com:8001 和 jack.com:8002 之间的 JSONP 请求:Github - nodejs-demo-1

阮一峰 - 浏览器同源政策及其规避方法

随它去吧 - 说说JSON和JSONP,也许你会豁然开朗,含jQuery用例

JSON官网


全局函数eval()有什么作用

eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。
eval() 是全局对象的一个函数属性

  • 如果 eval() 的参数是一个字符串。如果字符串表示的是表达式,eval() 会对表达式进行求值。如果参数表示一个或多个 JavaScript 语句,那么 eval() 就会执行这些语句。
  • 如果 eval() 的参数不是字符串,eval() 会将参数原封不动地返回。

MDN - eval()


为什么0.1+0.2!==0.3

0.1 + 0.2    // 0.30000000000000004

因为 JavaScript 存储数值采用双精度浮点数,会出现精度丢失的问题。

0.100000000000000000000002 === 0.1    // true
console.log(0.100000000000000002)    // 0.1

如何使得 0.1+0.2===0.3 呢?

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3    // true

掘金 - 0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作?


==、=== 与 Object.is()

  1. ==:相等运算符,会在比较时进行类型转换
  2. ===:严格运算符:比较时不进行隐式转换,类型不同则返回 false
  3. Object.is():该方法判断两个值是否为同一个值,返回 true / false。它不会强制转换两边的值。
  • 对于 string、number 等基础类型,是有区别的:
    1. 不同类型:==:转换成同一类型后的值看值是否相等;===:如果类型不同,结果就不等。
    2. 相同类型:直接进行值比较
  • 对于 Array、Object 等高级类型,是没有区别的
  • 对于基础类型与高级类型,是有区别的:
    1. ===:类型不同,结果不等;
    2. ==:将高级类型转化为基础类型,进行值比较。

Object.is() 方法如果满足以下条件则两个值相等:

  1. 都是 undefined;
  2. 都是 null;
  3. 都是 true 或 false;
  4. 都是相同长度的字符串且相同字符按相同顺序排列;
  5. 都是相同对象(意味着每个对象有同一个引用);
  6. 都是数字且都是 +0;都是 -0;都是 NaN;或都是非零而且非 NaN 且为同一个值;
Object.is('hello','hello')    // true
Object.is('hello','hi')   // false

Object.is([],[])    // false

Object.is(null,null)    // true
Object.is(null,undefined)   // false

Object.is(0,+0)   // true
Object.is(0,-0)   // false
Object.is(-0,+0)    // false
Object.is(NaN,0/0)    // true

扩展:

let obj = {
  name: 'LqZww'
}

if (obj.age == null) {}
// 等价于
if (obj.age === null || obj.age === undefined) {}

什么是原型

JavaScript中的原型与原型链

冴羽 - JavaScript深入之从原型到原型链


什么是闭包

JS中的闭包是什么?

冴羽 - JavaScript深入之闭包


什么是CORS,什么是跨域

跨域资源共享CORS详解


typeof与instanceof区别

typeof

typeof 可用于基本数据类型的类型判断,例如:number、string、boolean、function、undefined 等,返回值都是小写的字符串。

console.log(typeof undefined)   // undefined
console.log(typeof 1)   // number
console.log(typeof "a")   // string
console.log(typeof true)    // boolean
console.log(typeof Symbol())    // symbol

console.log(typeof function(){})    // function
console.log(typeof new Function())    // function

console.log(typeof null)    // object
console.log(typeof [1,2])   // object
console.log(typeof {})    // object
console.log(typeof new String())    // object
console.log(typeof new Object())    // object
console.log(typeof new Number())    // object

instanceof

instanceof 是判断变量是否为某个对象的实例,返回值为 true 或 false。

var arr = []
var obj = {}

console.log(arr instanceof Array)   // true
console.log(arr instanceof Object)    // true

console.log(obj instanceof Array)   // false
console.log(obj instanceof Object)    // true

MDN:instanceof运算符

区别

  1. typeof 用于基本数据类型的类型判断,无法判断对象的具体类型(除function)
  2. instanceof 可以用来区分数组、对象,不能用来判断字符串、数字等

如何实现深拷贝

JSON.parse()、JSON.stringify()

这种方法存在一个问题:能正确处理的对象只有 Number、String、Array 等能够被 JSON 表示的数据结构,因此像函数、undefined、正则表达式、引用这种不能被 JSON 表示的类型将不能被正确处理。

let obj = {
  name: 'LqZww',
  age: 18,
  like: {
    game: 'TLBB',
    ear: 'rou'
  },
  arr: [1, 2]
};

let deepClone = JSON.parse(JSON.stringify(obj));
deepClone.name = 'lq';
deepClone.like.game = 'LOL';
console.log(obj);    // {name: "LqZww", age: 18, like: {…}, arr: Array(2)}
console.log(deepClone);    // {name: "lq", age: 18, like: {…}, arr: Array(2)}
console.log(obj === deepClone);    // false

for...in + 递归

let obj = {
  name: 'LqZww',
  age: 18,
  like: {
    game: 'TLBB',
    ear: 'rou'
  },
  arr: [1, 2]
};

function deepClone(object) {
  let object2
  if (!(object instanceof Object)) {
    return object
  } else if (object instanceof Array) {
    object2 = []
  } else if (object instanceof Function) {
    object2 = eval(object.toString())
  } else if (object instanceof Object) {
    object2 = {}
  }
  for (let key in object) {
    object2[key] = deepClone(object[key])
  }
  return object2
}

var obj2 = deepClone(obj)
obj2.name = 'lq';
obj2.like.game = 'LOL';
console.log(obj)    // {name: "LqZww", age: 18, like: {…}, arr: Array(2)}
console.log(obj2)    // {name: "lq", age: 18, like: {…}, arr: Array(2)}
console.log(obj === obj2)    // false

Github - 详细代码


判断数组有哪几种方法

  1. instanceof
    instanceof 是判断变量是否为某个对象的实例,返回值为 true 或 false。

  2. Object.prototype.toString.call()
    每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。该方法对于所有基本的数据类型都能进行判断。

  3. Array.isArray()
    此方法用来判断对象是否为数组

参考代码


操作数组方法有哪些,分别有什么用

let arr = [11, 22, 33]
// 1. pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
let arr1 = arr.pop()
console.log(arr)    // (2) [11, 22]
console.log(arr1)    // 33
// 2. shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
let arr2 = arr.shift()
console.log(arr)    // (2) [22, 33]
console.log(arr2)    // 11
// 3. push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
let arr3 = arr.push(44)
console.log(arr)    // (4) [11, 22, 33, 44]
console.log(arr3)    // 4
// 4. unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度。
let arr4 = arr.unshift(1)
console.log(arr)    // (4) [1, 11, 22, 33]
console.log(arr4)    // 4
// 5. concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let arr5 = arr.concat([44, 55])
console.log(arr5)    // (5) [11, 22, 33, 44, 55]
// 6. slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end),原始数组不会被改变。
let arr6 = arr.slice(2)
console.log(arr6)    // [33]
// 7. filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素,不改变原数组。
let arr7 = arr.filter(num => num > 20)
console.log(arr7)    // (2) [22, 33]
// 8. map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
let arr8 = arr.map(num => num * 2)
console.log(arr8)    // (3) [22, 44, 66]
// 9. splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
let arr9 = arr.splice(1, 1, 44)
console.log(arr)    // (3) [11, 44, 33]
console.log(arr9)    // [22]

数组降维

二维数组

遍历

利用双重循环遍历二维数组中的每一个元素并存放到新的数组中。

var arr = [
  ['z', 'w', 'w'],
  ['l', 'q'],
  [12, 52, 7]
];
var result = [];
for (var i = 0; i < arr.length; i++) {
  for (var a = 0; a < arr[i].length; a++) {
    result.push(arr[i][a]);
  }
}
console.log(result);    // (8) ["z", "w", "w", "l", "q", 12, 52, 7]

concat()

利用 concat() 方法来合并两个或多个数组。

var arr = [
  ['z', 'w', 'w'],
  ['l', 'q'],
  [12, 52, 7]
];
var result = [];
for (var i = 0; i < arr.length; i++) {
  result = result.concat(arr[i]);
}
console.log(result);    // (8) ["z", "w", "w", "l", "q", 12, 52, 7]

concat + apply

var arr = [
  ['z', 'w', 'w'],
  ['l', 'q'],
  [12, 52, 7]
];
var result = Array.prototype.concat.apply([], arr);
console.log(result);    // (8) ["z", "w", "w", "l", "q", 12, 52, 7]

flat()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

var arr = [
  ['z', 'w', 'w'],
  ['l', 'q'],
  [12, 52, 7]
];
var result = arr.flat()
console.log(result)   // (8) ["z", "w", "w", "l", "q", 12, 52, 7]

多维数组

forEach + 递归

var arr = [
  1, [2, 3, [
    4, 5, [
      6, 7
    ], 8
  ], 9], 10
];
var result = []

function reduction(arr) {
  arr.forEach(item => {
    if (Array.isArray(item)) {
      reduction(item)
    } else {
      result.push(item)
    }
  })
  return result
}

console.log(reduction(arr));    // (10) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

flat()

flat() 的参数使用 Infinity,可展开任意深度的嵌套数组。

var arr = [
  1, [2, 3, [
    4, 5, [
      6, 7
    ], 8
  ], 9], 10
];
var result = arr.flat(Infinity)
console.log(result)   // (10) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

null与undefined的区别

null

null 是一个字面量,表示空,没有对象。

用法:

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

undefined

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

用法:

  1. 变量被声明了,但没有赋值时,就等于 undefined。
  2. 调用函数时,应该提供的参数却没有提供,该参数等于 undefined。
  3. 对象没有赋值的属性,该属性的值为 undefined。
  4. 函数没有返回值时,默认返回 undefined。

惯例:

  1. 如果一个变量没有赋值,那它就是 undefined。
  2. 如果一个对象 Object,现在还不想赋值,则可以给它一个 null,即:let obj = null,表示空对象。
  3. 如果有一个非对象,现在还不想赋值,则给它一个 undefined。

注意

null == undefined   // true
null === undefined    // false

typeof null   // "object"
typeof undefined    // "undefined"

1 + null    // 1
1 + undefined   // NaN

Promise、Promise.all、Promise.race分别怎么用

Promise从入门到放弃


什么是立即执行函数,使用立即执行函数的目的是什么

立即执行函数就是说这个函数是立即执行函数体的,不需要额外的去主动调用,要成为立即执行函数,需要满足两个条件:

  1. 声明一个匿名函数
  2. 立马调用这个匿名函数

立即执行函数的目的是 创建独立的作用域,让外部无法访问作用域内部的变量,从而避免 变量污染

下面这段代码就是一个立即执行函数:

(function(){
  console.log("这是立即执行函数")
})()

除了上面这种写法,还有如下写法:

// (匿名函数())
(function(){
  console.log("这是立即执行函数")
}())

// !匿名函数()
!function(){
  console.log("这是立即执行函数")
}()

// +匿名函数()
+function(){
  console.log("这是立即执行函数")
}()

// -匿名函数()
-function(){
  console.log("这是立即执行函数")
}()

// ~匿名函数()
~function(){
  console.log("这是立即执行函数")
}()

// void 匿名函数()
void function(){
  console.log("这是立即执行函数")
}()

// new 匿名函数()
new function(){
  console.log("这是立即执行函数")
}()

如何实现数组去重

传送门 - 数组去重的几种方法


如何用正则实现string.trim()

trim() 方法会从一个字符串的两端删除空白字符。

function trim(string) {
  return string.replace(/^\s+|\s+$/g, '')
}
console.log(trim("   LqZww   "))    // LqZww

了解ES6 class的用法吗

基本用法

在没有 ES6 class 之前的常规写法:

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function(sing) {
  console.log(this.name + "唱了" + sing)
}

var zname = new Person('zww', 22)
console.log(zname)    // Person {name: "zww", age: 22}
zname.sayHi('啊哈哈')   // zww唱了啊哈哈

在 ES6 中新增加了类的概念,可以使用 class 关键字声明一个类,之后以这个类来实例化对象。

类抽象了对象的公共部分,它泛指某一大类。

对象特指某一个,通过类实例化一个具体的对象。

class Star {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  sayHi(sing) {
    console.log(this.name + "唱了" + sing);
  }
}
var zname = new Star("zww", 11)
console.log(zname);   // Star {name: "zww", age: 11}
zname.sayHi("我爱你")   // zww唱了我爱你

注意:

  1. 类必须使用 new 实例化对象
  2. 通过 class 关键字创建类,类名首字母一般大写
  3. 类里面有个 constructor 函数,可以接收传递过来的参数,同时返回实例对象
  4. 类里面所有函数都不需要写 function
  5. 多个函数方法之间不需要用逗号隔开

类的继承

JavaScript 中的类可以继承某个类,其中被继承的类称为父类,而继承父类的被称为子类。

子类可以有自己的函数和构造器,当子类中存在父类相同的方法时,则该方法不会从父类继承,而使用子类的方法。

class Father {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  sayHi(sing) {
    console.log(this.name + '的年龄是' + this.age + ',并且唱了' + sing)
  }
}
class Son extends Father {

}
var father = new Father('zww', 18)
console.log(father)   // Father {name: "zww", age: 18}

var son = new Son('lq', 22)
console.log(son)    // Son {name: "lq", age: 22}
son.sayHi('呵呵')   // lq的年龄是22,并且唱了呵呵

super关键字

super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。

class Father {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  sayHi() {
    console.log('父类函数')
  }
}
class Son extends Father {
  constructor(name, age, sex) {
    super(name, age)
    this.sex = sex
  }
  sonfn() {
    super.sayHi()
    console.log('子类函数')
  }
}
var son = new Son('lq', 22, '女')
console.log(son)    // Son {name: "lq", age: 22, sex: "女"}
son.sonfn('丫丫')   // 父类函数 子类函数

注意:

  • 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象
  • 类里面的共有的属性和方法一定要加 this
  • this 的指向问题;constructor 里面的 this 指向的是创建的实例对象;方法里面的 this 指向这个方法的调用者

如何实现一个call函数

Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    console.log("error")
  }
  let args = [...arguments].slice(1)
  let result = null
  context = context || window
  context.fn = this
  result = context.fn(...args)
  delete context.fn
  return result
}

冴羽 - JavaScript深入之call和apply的模拟实现


如何实现一个apply函数

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError("Error")
  }
  let result = null
  context = context || window
  context.fn = this
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

如何实现一个bind函数

Function.prototype.myBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError("Error")
  }
  var args = [...arguments].slice(1),
    fn = this
  return function Fn() {
    return fn.apply(this instanceof Fn ? this : context, args.concat(...arguments))
  }
}

Github - 详细代码

冴羽 - JavaScript深入之bind的模拟实现


手写一个AJAX

let xhr = new XMLHttpRequest()
xhr.open('GET', '/xxx', true)
xhr.onreadystatechange = function(){
  if(xhr.readyState === 4){
    if(xhr.status >= 200 && xhr.status < 300){
      console.log('请求成功')
    }else{
      console.log('请求失败')
    }
  }
}
xhr.send()

AJAX从入门到放弃


手写一个函数防抖

当我们触发事件时,但是一定在事件触发的第 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那么就以新触发事件的时间为准,n 秒后才执行。

就是等你触发完事件 n 秒内不再触发事件才会执行。

function debounce(fn, wait) {
  let timer = null
  return function() {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => fn.apply(this, arguments), wait)
  }
}

示例代码 - 函数防抖

冴羽 - JavaScript专题之跟着underscore学防抖


手写一个函数节流

从上一次命令结束开始的一定时间范围 n 秒内,如果多次连续下达命令,则只执行当前时间段 n 秒内第一次命令。

如果你持续触发事件,每隔一段时间,只执行一次事件。

function throttle(fn, gapTime) {
  let lastTime = null
  let nowTime = null
  return function() {
    nowTime = Date.now()
    if (!lastTime || nowTime - lastTime > gapTime) {
      fn()
      lastTime = nowTime
    }
  }
}

示例代码 - 函数防抖

示例代码 - 自动保存案例

冴羽 - JavaScript专题之跟着 underscore 学节流


手写EventHub

class EventHub {
  private cache: { [key: string]: Array<(data: unknown) => void} = {}
  on(eventName: string, fn: (data: unknown) => void) {
    this.cache[eventName] = this.cache[eventName] || []
    this.cache[eventName].push(fn)
  }
  emit(eventName: string, data?: unknown) {
    let array = this.cache[eventName] || []
    array.forEach(fn => {
      fn(data)
    });
  }
  off(eventName: string, fn: (data: unknown) => void) {
    this.cache[eventName] = this.cache[eventName] || []
    let index = indexOf(this.cache[eventName], fn)
    if (index === -1) return;
    this.cache[eventName].splice(index, 1)
  }
}

export default EventHub;

Github - 详细代码


什么是事件委托

事件委托,其实就是把一个元素响应事件(click、keydown...)的函数委托到另一个元素上。

一般来说,我们会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

使用事件委托的好处:

  1. 可以减少内存的消耗
  2. 动态绑定事件

示例代码


待完善

async / await语法了解吗,目的是什么
手写一个Promise

posted @ 2020-08-26 23:54  LqZww  阅读(118)  评论(0编辑  收藏