JavaScript面试准备(二)

336 阅读6分钟

前言

继续冲冲冲!保佑实习上岸!

1. 对this的理解

this指向最后一次调用这个方法的对象

四种调用模式

  • 函数调用模式

当一个函数不是一个对象的属性时,直接作为函数调用时,this指向全局对象

  • 方法调用模式

当一个函数是一个对象的方法调用时,this指向这个对象

  • 构造器调用模式

当一个函数new一个实例时,函数执行前会创建一个新对象,this指向这个对象

  • apply、call、bind调用模式

apply:接收两个参数,一个是this绑定的对象,一个是参数数组

call:第一个是this绑定对象,后面的其余参数是传入函数执行的参数

bind:通过传入一个对象,返回一个this绑定了传入对象的心函数,这个函数的this指向了使用new时被改变,其余情况不会变

这四种模式,使用构造器调用模式的优先级最高,然后是apply、call和bind调用模式,然后是方法调用模式,然后是函数调用模式。

2. 实现call、apply、bind函数

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  3. 处理传入的参数,截取第一个参数后的所有参数。

  4. 将函数作为上下文对象的一个属性。

  5. 使用上下文对象来调用这个方法,并保存返回结果。

  6. 删除刚才新增的属性。

  7. 返回结果。

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
      result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
}

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  3. 将函数作为上下文对象的一个属性。

  4. 判断参数值是否传入

  5. 使用上下文对象来调用这个方法,并保存返回结果。

  6. 删除刚才新增的属性

  7. 返回结果

Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error")
  }
  let result = null
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window
  // 将函数设为对象的方法
  context.fn = this
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  // 将属性删除
  delete context.fn
  return result
}

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 保存当前函数的引用,获取其余传入参数值。

  3. 创建一个函数返回

  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。

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)
    )
  }
}

3. 箭头函数的this指向哪里

箭头函数没有自己的this,他的this时捕获所在上下文的this作为自己的this

由于没有属于自己的this,所以不会被new调用,这个this也不会被改变

可以⽤Babel理解⼀下箭头函数:

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj);
    }
  }
}

转化后:

// ES5,由 Babel 转译
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }
   } 
}

4. 箭头函数与普通函数的区别

  1. 比普通函数更加简洁

  2. 没有自己的this

  3. 继承来的this指向永远不会改变

  4. call()、apply()、bind()等方法不能改变箭头函数中this的指向

  5. 不能作为构造函数使用

  6. 没有自己的aruguments

  7. 没有prototype

  8. 不能用作Generator函数,不能使用yield关键字

5. 判断数组的方式有哪些?

  • instanceof

  • Array.isArray()

  • 原型链

  • Object.prototype.toString.call()

  • Array.prototype.isPrototypeOf()

6. 数组去重

双重for循环

function distinct(arr) {
  for(let i = 0; i < arr.length; i++) {
    for(let j = i+1; j < arr.length; j++) {
      if(arr[i] == arr[j]) {
        arr.splice(j, 1)
        arr.length--
        j--
      }
    }
  }
  return arr
}

Array.sort() + 冒泡

function distinct(array) {
    var res = [];
    var sortedArray = array.concat().sort();
    var seen;
    for (var i = 0, len = sortedArray.length; i < len; i++) {
        // 如果是第一个元素或者相邻的元素不相同
        if (!i || seen !== sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i];
    }
    return res;
}

Set去重

function distinct(arr) {
  return [..new Set(arr)]
}

Object 键值对去重

function distinct(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
​

如何答一道惊艳面试官的数组去重问题?

7. 类数组

类数组 (arguments) ,是传给函数参数的数组

类数组拥有和数组相似的方法,例如:length

类数组如何转数组

  • Array.prototype.slice.call(arguments)

  • Array.prototype.splice.call(arguments, 0)

  • Array.prototype.concat.apply( [], arguments)

  • Array.from(arguments)

8. JS继承

  • 原型链继承
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child'
}
Child.prototype = new Parent()
console.log(new Child())

虽然我们把child继承了parent,但是存在潜在问题

let child1 = new Child()
let child2 = new Child()

child1.play.push(2)

改变child1的play属性,child2也发生了改变。

这是因为两个实例共用同一个原型对象,内存空间是共享的。

  • 构造函数继承

借助 call 调用 parent 函数

function Parent() {
  this.name = 'parent'
}
​
Parent.prototype.getName = function() {
  console.log(this.name)
}
​
function Child() {
  Parent.call(this)
  this.type = 'child'
}
​
let cld = new Child()
console.log(cld)

父类中的属性都继承到了child上,但是Parent原型链中的属性和方法无法继承

只能继承父类的实例属性,不能继承原型属性和方法

  • 组合继承
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
​
Parent.prototype.getName = function() {
  console.log(this.name)
}
​
function Child() {
  Parent.call(this)
  this.type = 'child'
}
​
Child.prototype = new Parent()
Child.prototype.constructor = Childlet cld = new Child()
console.log(cld)

结合前面两种继承方式,虽然之前遇到的问题解决了,但是每创建一个Child实例都要重新new一个Parent实例,造成多构造一次的性能消耗

  • 原型式继承
let parent = {
  name: 'parent',
  play: [1, 2, 3],
  gerName: function() {
    return this.name
  }
}
​
let person = Object.create(parent)

这种方式也存在缺点,因为Object.create 方法实现的是浅拷贝,多个实例的引用类型指向相同的内存,存在数据污染的可能

  • 寄生式继承
let parent = {
  name: 'parent',
  play: [1, 2, 3],
  getName: function() {
    return this.name
  }
}
​
function extend(obj) {
  let clone = Object.create(obj)
  clone.getPlay = function() {
    return this.play
  }
  return clone
}
​
let person = extend(parent)

缺点和上面讲的原型式继承一样

  • 寄生组合式继承
function extend(parent, child) {
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
}
​
function Parent() {
  this.name = 'parent',
  this.play = [1, 2, 3]
}
​
Parent.prototype.getName = function() {
  return this.name
}
​
function Child() {
  Parent.call(this)
  this.type = 'child'
}
​
extend(Parent, Child)
​
Child.prototype.getPlay = function() {
  return this.type
}
​
let child = new Child()
console.log(child);

这是所有继承方式里面相对最优的继承方式

ES6中的extends实际采用的也是寄生组合继承方式