锦囊(JS)

184 阅读14分钟

1.原始数据类型有哪些?

7中:BigInt,symbol,number,string,null,boolean,undefined

注意,null不是对象,只是typeof null === 'object',原因是typeof方法内部是根据数据对应的二进制来判断的,前三位都是0,就认为是object,null的二进制表达全是0,所以结果是object

2.原始数据类型和引用数据类型有什么不同?

  • 原始数据类型保存在栈内存中,存储的是值。引用数据类型保存在堆内存中,存储的是内存地址(指针)。
  • js在进行变量赋值时,都是按值传递。原始数据类型会为每一个变量创建一份新的数据,而引用数据类型,多个变量会公用一份数据(每个变量保存的内存地址是一样的)

3.typeof能否正确的判断类型?

  • 对于原始数据类型,除了null以外,typeof都可以正确的判断
  • 对于引用数据类型,除了typeof function会返回function外,其余的都是object,所以,不能正确判断引用数据类型

4.this

  • this在代码执行时才能确定,定义时无法确定
  • 在构造函数中,this指向其new出来的实例
  • 在函数中,函数作为对象的属性执行,this指向该对象。函数在全局上下文中执行(正常调用),this指向全局对象
  • call,apply,bind可以改变函数中this的指向。指向的是传入的第一个参数
  • 箭头函数中的this:取决于箭头函数父作用域的this值。

5.描述new一个对象的过程

  • js内部会创建一个空对象
  • 将空对象的__proto__指向构造函数的原型对象
  • 然后把构造函数中的this指向该空对象(类似于call),执行构造函数,把对应的属性赋值给该对象
  • 最后返回空对象

6.谈谈你对原型以及原型链的理解?写一个原型链继承的例子?

原型:原型是一个存放实例方法的对象。在ES6以前,类是通过构造函数来实现的,为了实现多个实例之间的公有方法的封装,就把实例的公有方法定义在构造函数.prototype这个对象上,当实例访问某个方法时,会通过内部的指针去访问这个对象上对应的方法。

原型链:在构造函数A中,实例内部会有一个指针,指向构造函数的原型对象。如果把原型对象等于另一个构造函数B的实例,那么原型对象内部也有一个指针指向构造函数B的原型对象,这样层层递进,就形成了原型链。查找实例属性时,会按照原型链进行查找。

js是通过原型链实现继承的;

  • 原型式继承
function Super() {
  this.property = 'super'
}
Super.prototype.getProperty = function() {
  return this.property
}
//子类
function Sub() {
  this.property = 'sub'
}
Sub.prototype = new Super() //重写原型对象指向父类的实例
var sub = new Sub()
sub.getProperty() //sub

缺点是:原型对象中有引用数据类型的话,多个实例会公用一份数据

  • 组合继承
//父类
function Super(name) {
    this.name = name;
  	this.colors = ["red","white","yellow"];
}
Super.prototype.say=function(){
    console.log(this.name)
}
//子类
function Sub(name,age) {
    Super.call(this,name)
    this.age = age
}
Sub.prototype = new Super() //重写原型对象指向父类的实例
Sub.prototype.constructor = Sub

var sub1 = new Sub("xiaoli",20)
sub1.colors.push("pink")
console.log(sub1.colors) //["red","white","yellow","pink"]
var sub2 = new sub("xiaoming",22);
console.log(sub2.colors) //["red","white","yellow"]
  • Objece.create() 官方提供的实现原型继承的方法。
function object(o){
	function F(){
        
    }
    F.prototype = o
    return new F();
}

缺点:会调用两次超类的构造函数方法

7.谈谈你对作用域,作用域链的理解?

  • 作用域是由代码中函数声明的位置决定的,通过它,可以预测代码在执行过程中如何查找标识符。

  • js代码在执行时,会创建执行上下文。全局环境会创建全局执行上下文,函数会创建函数执行上下文,在执行上下文中,保存着当前执行环境中可以访问的变量和函数,这一块区域,叫做作用域。

  • 作用域链是指对查找变量的规则的一种描述。当我们在函数中访问一个变量时,引擎会在当前作用域中寻找,如果没有找到,就会去父作用域中找,直到全局作用域。如果没有找到,则会抛出ReferenceError.

8.谈谈你对闭包的理解?闭包的常见用途有哪些?

红宝书中:闭包是指有权访问另一个函数作用域中变量的函数

MDN中:闭包是指那些能够访问自由变量的函数,其中自由变量是指不是函数参数,也不是函数的局部变量。

通用的形成闭包的场景是在一个函数(A)中返回新的函数(B),并且新的函数B会访问函数A中的局部变量。

正常来说:函数A执行完成后,会弹出调用栈,同时其内部的局部变量会被销毁,但是根据作用域规则,函数B是可以访问函数A中的变量的,所以被函数B访问的变量并不会被销毁而是保存在内存中,这样就形成了闭包。这部分信息可以在浏览器的调用堆栈信息中看到。

闭包的常见用途:

  • var that = this
var name = 'window'
var obj = {
    name: 'obj',
    getName: function() {
        var that = this
        return function() {
            return that.name
        }
    }
}
var b = obj.getName()
b()

  • 封装私有变量:如gettersetter
function privateVariable() {
    var count = 0
    function getCount() {
        return count
    }
    function setCount(value) {
        count = value
    }
    return {
        getCount,
        setCount
    }
}
var count = privateVariable()
count.setCount(10)
console.log(count.getCount())
console.log(count.setCount(2))

9.什么是节流和防抖?如何实现?

节流和防抖,都是为了节省计算机资源,防止高频率的执行函数。

节流:指在指定时间间隔内只会执行一次任务。比如监听页面的scroll事件,每个固定的时间执行一次。

防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定的时间间隔,任务才会执行。比如监听inputchange事件,等用户停止输入了,才去执行。

简单的实现:

//节流
function throttle(fn, timeout) {
  var _flag = true;
  return function() {
    if (!_flag) {
      return;
    }
    _flag = false;
    setTimeout(() => {
      fn.call(this, ...arguments);
      _flag = true;
    }, timeout);
  };
}
//防抖
function debounce(fn) {
    let timeout = null; // 创建一个标记用来存放定时器的返回值
    return function () {
        // 每当用户输入的时候把前一个 setTimeout clear 掉
        clearTimeout(timeout); 
        // 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, 500);
    };
}

10.如何实现对象的深拷贝与浅拷贝?

浅拷贝

  • 利用扩展运算符实现
let copy1 = {...{x:1}}
  • Object.assign
let copy1 = Object.assign({},{x:1})
  • 循环
function shallowCopy(src) {
  let copy = {};
  for (let key in src) {
    //只拷贝实例属性,不拷贝原型属性
    if (obj.hasOwnProperty(key)) {
      copy[key] = src[key];
    }
  }
  return copy;
}

深拷贝

  • JSON.parseJSON.stringify
//无法拷贝函数,undefined等json不支持的格式
var copy = JSON.parse(JSON.stringify(src))
  • 递归调用
//递归调用
function deepCopy(src){
  let copy = {};
  for(let key in src){
    if(src.hasOwnProperty(key)){
      if(src[key] && typeof src[key]==="object"){
        copy[key] = deepCopy(src[key])
      }else {
        copy[key] = src[key]
      }
    }
  }
  return copy;
}
  • lodash第三方库

11.变量提升与暂时性死区?

变量提升的意思是,当用var声明一个变量,可以再声明变量位置之前访问这个变量,但值是undefined

原因就在于,js代码是先编译在执行的,编译的时候,会先把声明的变量和函数找到,然后提升到代码的开头(并不是物理位置改变,而是执行的时候),变量提升的时候,会给变量设置默认值undefined.

暂时性死区是let,const声明的变量,在变量初始化之前,变量会存在于暂时性死区当中,不能访问该变量。

其实let,const声明的变量,也会存在变量提升,创建变量的过程,分为三个阶段:创建,初始化,赋值.

var声明的变量,创建和初始化是一起完成的。但是let声明的变量,它创建了之后不会进行初始化,会存在与暂时性死区中,赋值的时候才进行初始化。

12.promise

(一句话概述promise):promise对象用于异步操作,相当于异步操作结果的占位符,表示一个尚未完成且预计在未来完成的异步操作。

(promise解决的问题):promise主要解决的是将回调嵌套(回调地狱)改为了then链式调用。实现的原理是,将then方法中的返回值封装成一个新的promise

(promise中的微任务):在回调模式中,异步任务完成之后,会将回调函数以及相应的参数,插入任务队列的末尾。在promise中,一旦promise的状态变为完成fulfilled或者拒绝rejected,会将then函数中对应的方法添加到微任务队列中。(这里其实使用到了观察者模式,promise的状态改变,就会触发then函数中绑定的方法)

(手写promise):

13.进程与线程

14.事件循环eventloop

说事件循环之前,先要知道任务队列的概念。

由于js的主要用途是处理交互,操作dom,所以它被设计成了单线程。这意味着,执行任务时,需要排队。如果一个任务耗时很长,不可能让线程一直等待下去,于是任务分为了同步任务与异步任务,耗时很长的任务会被当做异步任务来处理,处理的方式是:先发起任务,当任务执行完成时,会将其绑定的回调执行,由于异步任务执行的时间不确定,执行完成的时间点也不同,线程为了管理这些任务,就用到了消息队列

消息队列是先进先出的数据结构,当异步任务完成时,就会将绑定的回调函数插入队列的末尾,当同步任务都执行完成后,就会从消息队列中取出绑定的回调函数执行。

但是呢,执行回调函数时,有可能产生更多的任务,所以执行完成之后,又会循环的去取消息队列中的回调函数,这种获取消息,执行消息,再取消息的过程,就形成了一种循环。(取出一个消息并执行的过程,叫做一次循环)

那事件又是什么? 其实消息队列中的消息,都对应着一个事件。dom操作,对应的是dom事件;ajax中的回调,对应的是onreadystatechange事件;这种:循环触发特定事件->插入消息至任务队列->执行任务消息的过程,称为事件循环。

15.宏任务与微任务

微任务:promise,MutationObserver

宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI rendering

宏任务与微任务,本质上都是异步任务。

微任务产生的目的,是为了解决任务实时的问题。就拿监听dom变化来说,如果采用同步任务的方式,每次dom的变动,都去立即触发回调函数,那么会严重影响页面的性能,所以需要采用异步调用的方式,但是宏任务触发的回调非常影响实时性,因为两个任务之间,可能插入其他的事件,所以,就引入了微任务。微任务会在当前宏任务快要执行完了的时候执行,相比于创建一个宏任务,微任务会更快的执行。

微任务会比宏任务更快执行,是指的微任务会比下次的宏任务更快执行。因为微任务也是在上一次宏任务中产生的。举个例子:在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。(所以,微任务其实是会影响下一个宏任务的)。

script标签也是宏任务,所以代码执行前,后悔创建一个宏任务。

所以,MutationObserver 采用了“异步 + 微任务”的策略。

  • 通过异步操作解决了同步操作的性能问题;
  • 通过微任务解决了实时性的问题。

16.事件模型?

Dom事件的触发顺序:首先事件捕获,然后是事件处理执行,最后是事件冒泡。

addEventListener默认模式是冒泡.el.addEventListener("click",function(e){},false)

e.stopPropagation(),阻止事件的传递,事件捕获模式会阻止事件向下传递,事件冒泡会阻止事件向上传递。

e.preventDefault(),可以阻止某些标签的默认行为。

e.target:点击的目标元素

e.currentTarget:触发事件的元素

事件委托:给父元素绑定事件(e.currentTarget),点击子元素(e.target),根据冒泡机制,会触发父元素绑定的事件,然后根据e.target.nodeName可以判断点了哪个元素,执行相应的代码,这样减少了事件的注册,提升了性能。

17.要掌握的手写代码实现?

  • 手写call
Function.prototype.myCall = function (context, ...args) {
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  var key = Symbol();
  context[key] = this;
  var result = context[key](...args);
  delete context[key];
  return result;
};
  • 手写apply 区别在于传参不同。
Function.prototype.myApply = function (context, args) {
  if (typeof context === 'undefined' || context === null) {
    context = window;
  }
  var key = Symbol();
  context[key] = this;
  var result = context[key](...args);
  delete context[key];
  return result;
};
  • 手写bind bind转换后的函数可以作为构造函数使用。bind方法,其实是实现了函数柯里化。将一个函数的多个参数,返回为传入更少参数的函数
Function.prototype.myBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  let _this = this
  let args = [...arguments].slice(1)
  return function F() {
    // 判断是否被当做构造函数使用
    if (this instanceof F) {
      return _this.apply(this, args.concat([...arguments]))
    }
    return _this.apply(context, args.concat([...arguments]))
  }
}
  • 手写节流函数
function throttle(fn,wait) {
  let canRun = true; // 通过闭包保存一个标记
  return function() {
    // 在函数开头判断标记是否为true,不为truereturn
    if (!canRun) return;
    // 立即设置为false
    canRun = false;
    // 将外部传入的函数的执行放在setTimeout中
    setTimeout(() => {
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
      // 当定时器没有执行的时候标记永远是false,在开头被return掉
      fn.apply(this, arguments);
      canRun = true;
    }, wait);
  };
}
  • 手写防抖函数
function debounce(fn,wait) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
      // 每当用户输入的时候把前一个 setTimeout clear 掉
      clearTimeout(timeout); 
      // 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
      timeout = setTimeout(() => {
          fn.apply(this, arguments);
      }, wait);
  };

  • 手写Promise
class Promise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    try{
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    let promise2 = new Promise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    return promise2;
  }
  catch(fn){
    return this.then(null,fn);
  }
}
function resolvePromise(promise2, x, resolve, reject){
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') { 
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}
//resolve方法
Promise.resolve = function(val){
  return new Promise((resolve,reject)=>{
    resolve(val)
  });
}
//reject方法
Promise.reject = function(val){
  return new Promise((resolve,reject)=>{
    reject(val)
  });
}
//race方法 
Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data,resolve){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data,resolve);
      },reject);
    };
  });
}
  • 数组去重
//1.利用set集合
Array.from(new Set(arr))
//2.
function unique(arr) {
  var emptyArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (!emptyArr.includes(arr[i])) {
      emptyArr.push(arr[i]);
    }
  }
  return emptyArr;
}