前端面试题之js

2022/01/10 面试题

js面试常问知识点汇总

1、js中的数据类型

js种的数据类型分为:基础数据类型和复杂数据类型(对象类型/引用类型)

基础数据类型是存放在栈中,对象类型存放在堆中。

基础数据类型:

  • 在ES3时代:
    • 有5种基础数据类型:undefined、null、Boolean、number、string
    • 一种对象类型:object;array、function都是对象类型
  • 在ES6时代:基础数据类型新增了两种 symbol和bigint
    • symbol:代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
    • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

2、数据类型检测

2.1 typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

其中数组、对象、null都会被判断为object,其他判断都正确。

其中null判断为object是js遗留的问题,判断null的话可以null == null

2.2 instanceof

instanceof只能判断引用类型,而不能判断基础类型,因为内部运行机制是顺着原型链查找该对象的原型

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

2.3 constructor

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

3、箭头函数与普通函数的区别

  1. 箭头函数没有自己的this
  2. 箭头函数继承的this指向不可变,call()\bind()\apply()也不能改变
  3. 箭头函数不能作为构造函数使用
  4. 箭头函数没有arguments
  5. 箭头函数没有prototype
  6. 箭头函数不能用作generator函数,不能使用yeild关键字

4、proxy可以实现什么功能

proxy:Proxy(target, handler)

  1. target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  2. handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。可以进行get和set

在 Vue3.0 中通过 Proxy+Reflect 来替换原本的 Object.defineProperty 来实现数据响应式。

  • vue3响应原理代码:https://catsaid.cn/2020/04/20/vue3Proxy/

5、new

new的执行过程:

  1. 创建一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象
  3. 让函数的this指向这个对象,执行构造函数代码
  4. 判断函数的返回值类型,如果是值类型,就返回创建的对象,如果是引用类型,就返回引用类型的对象
function createObject(n, ...args){
  let obj = {}
  Object.setPrototypeOf(obj, n.prototype)
  let result = n.apply(obj, args)
  return result instanceof Object ? result : obj;
}

6、原型、原型链

在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

因为原型链的存在,而JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

原型修改:

function Funa(){
  // ...
}

Funa.prototype.getName = function(){
  return 'a'
}

let fun = new Funa()
console.log(fun.getName())  // a

原型的作用就是用于继承。

7、闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包的作用:

  1. 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  2. 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

经典面试题:循环中使用闭包解决 var 定义函数的问题

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

// 解决方案一:使用闭包
for (var i = 1; i <= 5; i++) {  
  ;(function(j) {    
    setTimeout(function timer() {      
      console.log(j)    
    }, j * 1000)  
  })(i)
}
// 其他方案:使用setTimeout的第三个参数或使用let

8、作用域、作用域链

  1. 全局作用域:
    1. 最外层函数和最外层函数外面定义的变量拥有全局作用域
    2. 所有未定义直接赋值的变量自动声明为全局作用域
    3. 所有window对象的属性拥有全局作用域
  2. 函数作用域:
    1. 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
    2. 作用域是分层的,内层作用域可以访问外层作用域,反之不行
  3. 块级作用域:
    1. 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
    2. 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链:在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用:保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

9、this

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  1. 函数调用:当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  2. 方法调用:如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  3. 构造器调用:如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  4. apply、call、bind调用:这三个方法都可以改变this的指向,改变到哪里this值的就是谁

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

10、call、apply、bind

call和apply的区别他们的作用都一样,区别是传入参数的形式不同

  • apply(obj,args):第一个参数是函数体的this的指向,第二个参数是一个带下标的集合,会传入函数体内,可以是数组或类数组,返回结果
  • call(obj, arg1, arg2…..):第一个参数是函数体的this的指向,后续参数会依次传入函数体内,返回结果
  • bind(obj, arg1, arg2…):第一个参数是函数体的this指向,后续参数会依次传入函数体内,返回的是一个函数

10.1 实现call()

实现步骤:

  1. 判断调用对象是否为函数
  2. 判断传入上下文对象是否存在,如果不存在则设置为window
  3. 处理参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果

实现代码:

let obj = {
  name: 'a',
  fun: function(){
    console.log('a')
    return this.name
  }
}

let obj2 = {
  name: 'b',
  fun: function(){
    console.log('b')
    return this.name
  }
}

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

console.log(obj.fun.myCall(obj2) ,'2222')// b

/*
{ name: 'b', fun: [Function: fun] }
function(){
        console.log('a')
      return this.name
    }
a
b 2222
*/

10.2 实现apply()

和call()差不多的逻辑

  1. 判断是否为函数
  2. 判断传入的上下文对象是否存在,不存在就设置为window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数是否传入
  5. 使用上下文对象来调用这个方法,并保存结果
  6. 删除刚才新增的属性
  7. 返回结果
let obj = {
    name: 'a',
    fun: function(c,d){
      return this.name + ',' +c + ','+d
    }
  }
  
  let obj2 = {
    name: 'b',
    fun: function(c,d){
      return this.name + ',' +c + ','+d
    }
  }


Function.prototype.myApply = function(context){
  if(typeof this !=='function'){
    console.log('type 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;
}

console.log(obj.fun.myApply(obj2,[2,3]))

/*
b,2,3
*/

10.3 实现bind()

步骤:

  1. 判断调用对象是否为函数
  2. 保存当前函数的引用,获取其余参数值
  3. 创建一个函数返回
  4. 函数内部使用apply来绑定函数调用,需要判断函数作为构造函数的情况,这个时候要传入当前函数的this给apply调用,其他情况都传入指定的上下文对象

let obj = {
  name: 'a',
  fun: function(){
    console.log([...arguments])
    return this.name
  }
}

let obj2 = {
  name: 'b',
  fun: function(){
    return this.name
  }
}


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

f = obj.fun.myBind(obj2,2,3,4,5,6,7)
console.log(f()) 
/*
[ 2, 3, 4, 5, 6, 7 ]
b
*/ 

11、异步编程

JavaScript中异步执行有以下几种方法:

  1. 回调
  2. promise
  3. generator
  4. async+await

12、promise

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

Promise本身是同步的立即执行函数, 当在执行resolve或者reject的时候, 此时是异步操作,会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。

promise有三种状态:

  1. 进行中
  2. 已完成
  3. 已拒绝

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。一旦从进行状态变成为其他状态就永远不能更改状态了。

promise的缺点:

  1. 无法取消,一旦新建它就会立即执行,无法中途取消
  2. 如果不设置回调,promise就会抛出错误,不会反应到外部
  3. 当处于pending状态时,无法得知目前进展到哪个阶段(刚刚开始还是即将结束)

基本用法:

const promise = new Peomise(function(resolve, reject){
  if(true){
    resolve()
  }else{
    reject()
  }
})
promise.then((res)=>console.log(res))

// 或直接使用Promise.resolve(value)
Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

// Promise.reject()也可以直接使用
Promise.reject(new Error(error));

12.1 promise的方法

有五个常用方法:

  1. then():当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

  2. catch():Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

  3. All():方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

    let promise1 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           resolve(1);
    	},2000)
    });
    let promise2 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           resolve(2);
    	},1000)
    });
    let promise3 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           resolve(3);
    	},3000)
    });
    Promise.all([promise1,promise2,promise3]).then(res=>{
        console.log(res);
        //结果为:[1,2,3] 
    })
    
  4. rece():方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

    let promise1 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           reject(1);
    	},2000)
    });
    let promise2 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           resolve(2);
    	},1000)
    });
    let promise3 = new Promise((resolve,reject)=>{
    	setTimeout(()=>{
           resolve(3);
    	},3000)
    });
    Promise.race([promise1,promise2,promise3]).then(res=>{
    	console.log(res);
    	//结果:2
    },rej=>{
        console.log(rej)};
    )
    
  5. Finally():方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

12.2 all()和race()的区别

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

需要注意的是promise.all()在传入多个promise对象之后执行顺序是不固定的,但是返回的数据和传入的数据是对应的。

Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

13、async/await

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中

async 函数返回的是一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async function test(){
  //...
  return x;
}
let a = await test()

// 如果不能使用await可以继续使用.then()
let a = test()
a.then(res=>console.log(res))

await是等待的意思,语法要求需要在async函数内使用,但是await不一定需要非得等待promise结果,可以是ajax或其他函数的结果

假设有一个业务,有多个步骤,每个步骤都依靠前一个步骤的结果来完成,那么就需要使用await来把并行改成串行

异常捕获:

async function test(){
  try{
    // ...
  }
  catch(){
    // ...
  }
}

14、eventloop 事件环

事件环就是代码的执行顺序,JS进程中主线程执行完毕后会走异步代码,因为JS是单线程,所以进程中只有一条线程,js的异步事件会再次开辟一条线程不会马上执行( 不是主线程 ),叫做任务队列,任务队列有宏任务与微任务之分。

14.1 宏任务

宏任务每次取一个,微任务每次取一组。

等主线程全部执行完毕,清空微任务队列之后,会取出宏任务队列里的第一个宏任务放到主线程里面执行,如果执行过程中遇到微任务会把他添加到微任务队列,执行完当前宏任务后立即执行微任务队列的任务,然后再去宏任务队列里取下一个宏任务。执行过程中遇到宏任务会放到宏任务队列的最后一个位置等待执行。

  • js的宏任务:script、Ajax、事件、requestFrameAnimation、定时器。
  • node的宏任务:I/O 视图渲染。

14.2 微任务

微任务会比宏任务快,但是script例外,因为js是script脚本,所以会先执行script

  • 微任务:promise.then(promise的参数函数是立即执行的)、 MutationObserver、process.nextTick

15、面向对象

面向对象有三大特性,封装、继承和多态。对于ES5来说,没有class的概念,并且由于js的函数级作用域(在函数内部的变量在函数外访问不到),所以我们就可以模拟 class的概念,在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。

  • 封装:将属性和方法组成一个类的过程就是封装。
  • 继承:js继承是通过原型链进行继承的
  • 多态:js函数是无态的,js不支持多态

15.1 封装

说一下对象的几种常见创建方式:

  1. 字面量形式直接创建let obj = {}

  2. 工厂模式:工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

    function createPerson(name,age){
      let o = new Object();
      o.name = name;
      o.age = age;
      o.sayName = function(){
        return this.name
      }
      return o;
    }
    let p1 = CreatePerson('a',20)
    

    在使用工厂模式创建对象的时候,我们都可以注意到,在createPerson函数中,返回的是一个对象。那么我们就无法判断返回的对象究竟是一个什么样的类型。所以就出现了构造函数创建对象方法

  3. 构造函数创建对象:js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。

  4. 执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

    function Person(name, age){
      this.name = name;
      this.age = age;
      this.sayName = function(){
        console.log(this.name)
      }
    }
    let p1 = new Person('a', 20);
    p1.sayName() // a
       
    /*
    对比工厂模式,构造函数有很明显的优点
    1. 没有明显的创建对象,而是挂到了this上
    2. 没有return
    3. 可以检测对象的类型 p1 instanceof Person
       
    缺点就是sayName()方法会在每个实例上都创建
    */
    
  5. 原型模式:原型模式就是把函数体内的this挂载,改为挂到函数的原型上面prototype,这样创建的实例都可以共享它包含的属性和方法

    function Person(){}
    Person.prototype={
      name: 'a',
      age: 20,
      sayName: function(){
        console.log(this.name)
      }
    }
    var p1 = new Person();
    p1.sayName();
    

    这样就会有新的问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

  6. 组合使用构造函数模式和原型模式:属性都是在构造函数中定义的,通过原型对象来实现函数方法的复用,但是这种对代码的封装性不太好。

    function Person(name, age){
      this.name = name;
      this.age = age;
    }
       
    Person.prototype={
      constructor:Person,
      sayName: function(){
        console.log(this.name)
      }
    }
    

每种创建的方法都有自己的优势和缺点,选择最适合的方法创建即可。

15.2 继承

在js中继承就是通过原型链进行模拟实现继承的,几种常见的继承的方法如下:

  1. 类式继承:就是子类的原型是父类的实例化对象。

    function Super(){}
    function Son(){}
    Son.prototype = new Super()
       
    /*
    优点:子类实例化对象的属性和方法都指向父类的原型
    缺点:子类之间可能会互相影响
    */
    
  2. 构造函数继承:构造函数继承就是在子类函数体内进行call()

    function Super(){}
    function Son(){
      Super.call(this)
    }
    /*
    优点:每个实例化的子类互不影响
    缺点:内存浪费
    */
    
  3. 组合式继承:组合式继承就是汲取两者的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。

    function Super(){}
    function Son(){
      Super.call(this)
    }
    Son.prototype = new Super();
    let s1 = new Son();
    /*
    缺点:父类的构造函数会被创建两次 Super.call(this) 、Son.prototype = new Super();
    */
    
  4. 寄生组合继承:在组合继承中,父类会被创建两次,规避这个问题就可以了,所以说我们先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了~

    //原型式继承
    //原型式继承其实就是类式继承的封装,实现的功能是返回一个实例,改实例的原型继承了传入的o对象
    function inheritObject(o) {
      //声明一个过渡函数对象
      function F() {}
      //过渡对象的原型继承父对象
      F.prototype = o;
      //返回一个过渡对象的实例,该实例的原型继承了父对象
      return new F();
    }
    //寄生式继承
    //寄生式继承就是对原型继承的第二次封装,使得子类的原型等于父类的原型。并且在第二次封装的过程中对继承的对象进行了扩展
    function inheritPrototype(subClass, superClass){
      //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型
      var p = inheritObject(superClass.prototype);
      //修正因为重写子类原型导致子类constructor属性被修改
      p.constructor = subClass;
      //设置子类的原型
      subClass.prototype = p;
    }
    //定义父类
    var SuperClass = function (name) {
      this.name = name;
      this.books = ['javascript','html','css']
    };
    //定义父类原型方法
    SuperClass.prototype.getBooks = function () {
      console.log(this.books)
    };
       
    //定义子类
    var SubClass = function (name) {
      SuperClass.call(this,name)
    }
       
    inheritPrototype(SubClass,SuperClass);
       
    var subclass1 = new SubClass('php')
    

16、深浅拷贝

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

拷贝完善的一篇可以参考https://segmentfault.com/a/1190000016672263

16.1 浅拷贝实现

  1. Object.assign():可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

    let obj1 = {a:1}
    let obj2 = {b:2}
    let obj3 = Object.assign({},obj1, obj2)
    console.log(obj3) // {a: 1, b: 2}
    
  2. 展开运算符...

    let obj = {a:1, b:2}
    let obj1= {...obj}
    
  3. Array.prototype.concat():用于合并两个或者多个数组。

    const arr1 = [
      1,
      {
        user: 'a'
      }
    ];
    let arr2 = arr1.concat();
    arr2[0] = 2;
    arr2[1].user = 'b';
    console.log(arr1); // [ 1, { user: 'b' } ]
    console.log(arr2); // [ 2, { user: 'b' } ]
    
  4. Array.prototype.slice():数组的一个内置方法,该方法会返回一个新的对象,不改变原数组

    const arr1 = [
      1,
      {
        user: 'a'
      }
    ];
    let arr2 = arr1.slice();
    arr2[0] = 2;
    arr2[1].user = 'b';
    console.log(arr1); // [ 1, { user: 'b' } ]
    console.log(arr2); // [ 2, { user: 'b' } ]
    
  5. 手写:就是一个for循环,然后进行遍历赋值

    const shallowClone = (obj) => {
      const dst = [];
      for (let prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            dst[prop] = obj[prop];
        }
      }
      return dst;
    }
    

16.2 深拷贝

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。用上面的浅拷贝修改一下:

const deepClone = (obj) => {
  var dst = Array.isArray(obj) ? [] : {};
  if(!obj){
    return obj;
  }
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      if(typeof obj[prop] === 'object' && obj[prop]){
        dst[prop] = deepClone(obj[prop])
      }
        dst[prop] = obj[prop];
    }
  }
  return dst;
}

这就实现了一个简单的深拷贝,另外还可以使用JSON进行深拷贝

JSON.parse(JSON.stringify(object))

这样会有一些其他的问题:

  1. 会忽略undefined
  2. 会忽略symbol
  3. 不能处理正则
  4. 不能处理new Date()
  5. 不能解决循环引用的对象
  6. 不能序列化函数

17、防抖节流

  • 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
  • 节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

防抖实现:

let timer = null;
function btnClick(){
  clearTimeout(timer)
  timer = setTimeout(()=>{
    // 一些操作
  },1000)
}

/*
原理就是,每次触发事件都把之前的定时任务清除,然后重新开一个任务
*/

节流实现:

let timer = null;
function btnClick(){
  if(!timer){
    timer = setTimeout(()=>{
      // 一些操作
      timer = null;
    },1000)
  }
}
/*
原理就是当timer存在的时候,不给触发操作,执行完了把timer清空就可以重新触发了
*/

18、高阶函数

高阶函数很简单,满足以下任意一个条件的都是高阶函数

  1. 接受一个或多个函数作为输入
  2. 输出一个函数

接收一个函数的例子如:Array.prototype.map()方法接受一个回调,接受回调的都是高阶函数,因为这是一个函数

返回一个函数的例子:

function add(a) {
    function sum(b) { // 使用闭包
    	a = a + b; // 累加
    	return sum;
    }
    sum.toString = function() { // 重写toString()方法
        return a;
    }
    return sum; // 返回一个函数
}

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10 

每次调用都是返回了一个函数sum()

18.1 柯里化

柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。如bind()就是一个柯里化函数,bind在内部处理了第一个参数,然后交给了apply()执行,把其余参数都给apply()传过去了。

a.bind(b,1,2,3,4)
/*
bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind返回的绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
*/

作用:

  1. 延迟计算
  2. 动态创建函数
  3. 参数复用

18.2 反柯里化

如果说柯里化的过程是将函数拆分成功能更具体化的函数,那反柯里化的作用则在于扩大函数的适用性,使本来作为特定对象所拥有的功能函数可以被任意对象所使用。

// 构造函数 F
function F() {}

// 拼接属性值的方法
F.prototype.concatProps = function () {
    let args = Array.from(arguments);
    return args.reduce((prev, next) => `${this[prev]}&${this[next]}`);
}

// 使用 concatProps 的对象
let obj = {
    name: "Panda",
    age: 16
};

// 使用反柯里化进行转化
let concatProps = uncurring(F.prototype.concatProps);

concatProps(obj, "name", "age"); // Panda&16

Search

    Table of Contents