前端面试题(js基础)

461 阅读1小时+

面试提问区

1.介绍一下闭包是什么?

答:闭包是指一个能够访问外部函数自由变量的函数,并且持续性的对这个变量进行引用。即使外部函数执行上下文被销毁,被引用的自由变量仍然在内存中

2.闭包是怎么形成的?

答:在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁,但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

3.闭包是如何查找自由变量的?

答:自由变量的查找,是在函数定义的地方,向上级作用域查找,而不是在执行的地方

4. call、apply、bind 实现

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

var obj = {
  value: "vortesnail",
};

function fn() {
  console.log(this.value);
}

fn.call(obj); // vortesnail

等同于下面

var obj = {
  value: "vortesnail",
  fn: function () {
    console.log(this.value);
  },
};

obj.fn(); // vortesnail

所以自己写的call应该原理就是将fn添加到obj中,从而实现改变指针的目的。 自定义call实现如下:

Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 首先获取参数
  let args = [...arguments].slice(1);
  let result = null;
  // 判断 context 是否传入,如果没有传就设置为 window
  context = context || window;
  // 将被调用的方法设置为 context 的属性
  // this 即为我们要调用的方法
  context.fn = this;
  // 执行要被调用的方法
  result = context.fn(...args);
  // 删除手动增加的属性方法
  delete context.fn;
  // 将执行结果返回
  return result;
};

apply 我们会了 call 的实现之后,apply 就变得很简单了,他们没有任何区别,除了传参方式

const person = {
  firstName: 'John',
  lastName: 'Doe',
  fullName: function() {
    return `${this.firstName} ${this.lastName}`;
  }
};

const person2 = {
  firstName: 'Jane',
  lastName: 'Doe'
};

console.log(person.fullName.apply(person2)); // Jane Doe

如何实现apply:

Function.prototype.myApply = function (context) {
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  let result = null;
  context = context || window;
  // 与上面代码相比,我们使用 Symbol 来保证属性唯一
  // 也就是保证不会重写用户自己原来定义在 context 中的同名属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 执行要被调用的方法
  if (arguments[1]) {
    result = context[fnSymbol](...arguments[1]);
  } else {
    result = context[fnSymbol]();
  }
  delete context[fnSymbol];
  return result;
};

bind bind 返回的是一个函数

用法实例:

const person = {
  firstName: 'John',
  lastName: 'Doe',
  fullName: function() {
    return `${this.firstName} ${this.lastName}`;
  }
};

const person2 = {
  firstName: 'Jane',
  lastName: 'Doe'
};

const fullName = person.fullName.bind(person2);
console.log(fullName()); // Jane Doe

如何实现:

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 获取参数
  const args = [...arguments].slice(1),
  const fn = this;
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context,
      // 当前的这个 arguments 是指 Fn 的参数
      args.concat(...arguments)
    );
  };
};

5.为什么JavaScript不能有多个线程呢?(Why?)

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

6.js是一门什么语言?

答:javascript是一门单线程语言,也就是说,同一个时间只能做一件事,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的。

image.png

  • GUI 渲染线程

    • 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
    • 页面重绘和回流
    • 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
  • JS 引擎线程

    • 负责 JS 脚本代码的执行
    • 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
    • GUI 渲染线程互斥 ,执行时间过长将阻塞页面的渲染
  • 事件触发线程

    • 负责将准备好的事件交给 JS 引擎线程执行
    • 多个事件加入任务队列的时候需要排队等待(JS 的单线程)
  • 定时器触发线程

    • 负责执行异步的定时器类的事件,如 setTimeout、setInterval
    • 定时器到时间之后把注册的回调加到任务队列的队尾
  • HTTP 请求线程

    • 负责执行异步请求

    • 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列

7.宏任务(macro-task)和微任务(micro-task)有哪些?他们执行顺序是怎么样的?

答:macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate,UI交互时间,I/O事件,requestAnimationFrame micro-stask(微任务):Promise(then、catch、finally里面的代码),process.nextTick,MutationObserver

执行顺序: 事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

8.如何实现一个promise.all?

Promise.all()的特点是,只有当所有的Promise对象都成功地执行(即状态为resolved)时,返回的Promise对象才会成功地执行,并将所有Promise对象的结果按照传入的顺序组成一个数组返回;否则,只要有任意一个Promise对象失败(即状态为rejected),返回的Promise对象就会失败,并将第一个失败的Promise对象的错误信息作为自己的错误信息。

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    // 参数可以不是数组,但必须具有 Iterator 接口
    if (typeof promises[Symbol.iterator] !== "function") {
      reject("Type error");
    }
    if (promises.length === 0) {
      resolve([]);
    } else {
      const res = [];
      let count = 0;
      const len = promises.length;
      for (let i = 0; i < len; i++) {
        //考虑到 promises[i] 可能是 thenable 对象也可能是普通值
        Promise.resolve(promises[i])
          .then((data) => {
            res[i] = data;
            if (++count === len) {
              resolve(res);
            }
          })
          .catch((err) => {
            reject(err);
          });
      }
    }
  });
};

9. 介绍一下 async/await ,和Promise有什么关系?

答:async/await是ES2017引入的一种异步编程的解决方案,它基于Promise对象实现,使得异步编程更加简洁、易读和可维护。 执行 async 函数,返回的一定是 Promise 对象。await 相当于 Promise 的 then。try...catch 可捕获异常,代替了 Promise 的 catch。

14.给我介绍一下cookie、localstorage和sessionStorage?

1. Cookie

Cookie是浏览器存储少量数据的一种机制,通常用于存储用户的身份认证、会话标识等,Cookie可以设置过期时间,且会随着http请求发送至服务器端,会导致网络性能问题。Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的。

**Cookie的使用场景:**

  - 最常见的使用场景就是Cookie和session结合使用,我们将sessionId存储到Cookie中,每次发请求都会携带这个sessionId,这样服务端就知道是谁发起的请求,从而响应相应的信息。
  - 可以用来统计页面的点击次数

2. localStorage

localStorage是HTML5提供的一种本地存储机制,可以用于存储较大量的数据,存储大小为5M,localStorage可以设置key-value对,存储的数据类型为字符串。localStorage存储的数据是永久性的,除非用户手动清除,否则会一直存在浏览器中。LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问。

3. sessionStorage

sessionStorage是HTML5提供的一种本地存储机制,但是他与localStorage的区别在于,sessionStorage存储的数据是会话级别的,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

补充:

SessionStorage与LocalStorage对比:

  • SessionStorage和LocalStorage都在本地进行数据存储

  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享

  • LocalStorage和SessionStorage都不能被爬虫爬取

15.给我介绍一下http状态码?

  • 1xx - 服务器收到请求。
  • 2xx - 请求成功,如 200。
  • 3xx - 重定向,如 302。
  • 4xx - 客户端错误,如 404。
  • 5xx - 服务端错误,如 500。

16.常见的http状态码分别代表什么含义?

  • 200 - 成功
  • 301 - 永久重定向
  • 302 - 临时重定向
  • 304 - 资源未被修改
  • 403 - 没有权限
  • 404 - 资源未找到
  • 500 - 服务器错误
  • 504 - 网关超时

17. 什么是http缓存?

HTTP缓存是一种机制,用于在客户端(如浏览器)和服务器之间缓存HTTP响应,以提高性能和减少网络流量。HTTP缓存可以存在多个地方,包括浏览器缓存、代理服务器缓存和CDN缓存等。比如,游览器会将下载的资源(HTML、CSS、JavaScript、图片等)缓存在本地,下次再访问同一资源时,游览器可以直接从本地缓存中读取,而不必重新下载。

18.为什么需要缓存?

答:网络请求相比于CPU的计算和页面渲染是非常非常慢的。

19.缓存分为哪几种?

强制缓存: 当游览器第一次请求资源时,服务器会返回资源的同时,告诉游览器这个资源在一段时间内不需要再次请求,这个时间就是缓存时间,当游览器再次请求该资源时,会先检查本地缓存是否过期,如果没有过期,就直接使用本地缓存,如果过期了才会重新向服务器请求该资源。

协商缓存: 当游览器第一次请求资源时,服务器会返回资源的同时,告诉游览器这个资源在一段时间内不需要再次请求,并且返回一个标识该资源的唯一标识符(如ETag)。当游览器再次请求该资源时,会向服务器发送一个请求头信息,包含上次请求时服务器返回的唯一标识符,并询问该资源是否有更新,如果服务器发现该资源已经更新,则返回一个新的资源,并更新唯一标识符,让游览器重新缓存该资源。

20.GET和POST区别?

答: 1.GET请求会将请求参数通过URL传递,而POST请求会将请求参数放在请求体中传递。 2.GET请求的参数的长度有限制,一般是1024字节左右,而POST请求的参数无限制。 3.GET请求会被游览器缓存,POST请求不会被游览器缓存。 总之,GET 请求适用于请求数据量小、请求参数简单、安全性要求不高的场景,而 POST 请求适用于请求数据量大、请求参数复杂、安全性要求高的场景。

21.什么情况会导致跨域?(跨域是什么)

答:跨域问题是指浏览器限制了从一个源(协议+域名+端口)加载的文档或脚本如何与来自其他源的资源进行交互。以下情况会导致跨域问题:

  • 1.协议不同:如http和https。
  • 2.域名不同:如www.example.com 和 api.example.com。
  • 3.端口不同:如 www.example.com:8080www.example.com:9090。 任何两个页面的协议、域名、端口号中只要有一个不同,就会产生跨域问题。

22. React 事件机制,React 16 和 React 17 事件机制的不同?

React16的事件处理流程: 1.React在document上绑定事件监听器。 2.用户触发事件,事件冒泡到document上。 3.React通过事件委托机制将事件传递到对应的组件上。 4.组件通过SyntheticEvent对象获取事件信息,并执行事件处理函数。

React17的事件处理流程: 1.React直接在组件上绑定事件监听器。 2.用户触发事件,事件直接传递到组件上。 3.组价通过SyntheticEvent对象获取事件信息,并执行事件处理函数。 总之,React 17 的事件处理方式更加直接和高效,可以减少事件冒泡和事件委托带来的性能损失,从而提高应用程序的性能和响应速度。

23.React的生命周期

React16之前: 在这里插入图片描述 初始化阶段: 发生在constructor中的内容,在constructor中进行state、props的初始化,在这个阶段修改state,不会执行更新阶段的生命周期,可以直接对state赋值。

挂载阶段: componentWillMount: 发生在render函数之前,还没有挂载Dom render componentDidMount:发生在render函数之后,已经挂载Dom

更新阶段: 更新分别是由state更新引起和props更新引起。 props更新时:

  • 1.componentWillReceiveProps(nextProps, nextState) 这个生命周期主要为我们提供对props发生改变的监听,如果你需要在props发生改变后,相应改变组件的一些state.在这个方法中改变 state 不会二次渲染,而是直接合并 state。
  • 2.shouldComponentUpdate(nextProps, nextState) 这个生命周期需要返回一个Boolean类型的值,判断是否需要更新渲染组件,优化react应用的主要手段之一,当返回false就不会向下执行生命周期了,在这个阶段不可以setState(),会导致循环引用。
  • 3.componentWillUpdate(nextProps, nextState) 这个生命周期主要给我们一个世纪能够处理一些在Dom发生更新之前的事情,如获得Dom更新前某些元素的坐标,大小等。

到目前为止,this.props和this.state都未发生更新。


  • 4.render
  • 5.componentDidUpdate(prevProps, prevState) 在此时已经完成渲染,Dom已经发生变化,state已经发生更新,prevProps、prevState均为上一个状态的值。

state更新时(具体同上) 1.shouldComponentUpdate 2.componentWillUpdate 3.render 4.componetDidupdate

卸载阶段: 在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等。componentWillUnmount 中不应调用 setState,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

React16之后: 在这里插入图片描述

24.为什么废弃componentWillMount、componentWillUpdate、componentWillReceiveProps三个生命周期?

答:主要由于这三个生命周期函数可能会被误用,例如在componentWillReceiveProps函数中更新state,这可能导致死循环等问题。

25.React中setState() 同步还是异步?

答:setState()本身是一种同步的方法,但是一旦走了react内部的合并逻辑,放入了updateQueue队列中就变成了异步了。 异步情况: 由React控制的事件处理函数,以及生命周期函数调用setState时表现为异步。 大部分开发中用到都是React封装的事件,比如onChange、onClick、onTouchMove等,这些事件处理函数中setState都是异步处理。

同步情况: React控制之外的事件中调用setState是同步更新的。 还有就是原生js绑定的事件,setTimeout/setInterval,ajax,promise.then等setState也是同步的。

原理: 在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates判断是直接更新 this.state 还是放到一个updateQueue中延时更新。 而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state。 但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true。 而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true。 这样由 React 控制的事件处理过程 setState 不会同步更新 this.state,而是异步的。

26.React中useCallback和useMemo的区别是什么?

答: useCallback的作用是缓存函数,避免每次渲染都创建新的函数,当组件的props和state发生变化时,缓存的函数不会重新创建,而是返回缓存的函数的引用,从而避免再次不必要的渲染。第一个参数是一个函数,第二个参数是一个依赖数组。当依赖数组中的值发生变化时,缓存函数会被重新创建。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useMemo的作用是缓存计算结果,避免每次缓存都重新计算,当组件的props和state发生变化时,缓存的结果不会重新计算,而是返回缓存的结果,从而避免不必要的渲染,第一个参数是一个函数,用于计算结果,第二个函数是一个依赖数组,当依赖数组中的值发生变化时,缓存的结果会重新计算。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback缓存的是一个函数,适用于需要传递给子组件的函数,例如事件处理函数。而useMemo缓存的是一个值,适用于需要计算的值,例如复杂的计算结果。

27.为什么不能在条件语句(循环,嵌套函数)中写 hook?

答:React的hook是通过链表来实现hook的查找的,每次hook的调用都对应一个全局的index索引,如果采用条件语句,hook可能在不同的条件下多次调用,导致无法在链表中查找出来hook,从而导致组件状态不一致或试图更新不正确的问题。

28.useEffect依赖为空数组与componentDidMount区别?

componentDidMount会在render执行以后,立刻执行,也就是在虚拟dom之后执行,如果这时候,在生命周期中再一次setState,会导致render重新执行,但是真实游览器dom只会渲染第二次的值,这样可以避免闪屏。 useEffect是在真实的DOM渲染之后才会执行,这会造成两次真实渲染,从而有可能会闪屏。

29.介绍一下DOM-diff的整个流程?

答: 1.用JS对象模拟DOM(虚拟DOM) 2.用此虚拟DOM转换成真实DOM并插入页面中(render) 3.如果有事件发生修改了虚拟DOM,比较两颗虚拟DOM树的差异,得到差异对象(diff) 4.把差异对象应用到真正的DOM树上(patch)

30.有哪些方式可以优化前端性能?

代码层面:

  • 防抖和节流

  • 减少重排和重绘

  • 事件委托

  • css放js脚本的最底部

  • 减少DOM操作

  • 按需加载,比如React中使用React.lazy和React.Suspense。通常需要与webpack中 splitChunks配合。 构建方面:

  • 压缩代码文件,在 webpack 中使用 terser-webpack-plugin 压缩 Javascript 代码;使用 css-minimizer-webpack-plugin 压缩 CSS 代码;使用 html-webpack-plugin 压缩 html 代码。

  • 开启 gzip 压缩,webpack 中使用 compression-webpack-plugin ,node 作为服务器也要开启,使用 compression。 其他:

  • 图片压缩。

  • 使用 http 缓存,比如服务端的响应中添加 Cache-Control / Expires 。

31. 介绍一下src和href的区别?

答:src表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置,src会将其指向的资源下载并应用到文档内,如请求js脚本。href表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系。当浏览器识到他指向的文件时,就会并行下载资源,不会停止对当前文档的处理。

32.介绍一下HTML语义化?

答:语义化是指根据内容的结构化,选择合适的标签,通俗来讲就是用正确的标签做正确的事情。优点是,对机器友好,有利于SEO,对开发者友好,可读性更强。 常用的语义化标签:

<header></header>  头部

<nav></nav>  导航栏

<section></section>  区块(有语义化的div)

<main></main>  主要区域

<article></article>  主要内容

<aside></aside>  侧边栏

<footer></footer>  底部

33.DOCTYPE(文档类型)的作用是什么?

答:DOCTYPE是HTML5中一种标准的通用标记语言的文档类型声明,它的目的是告诉浏览器应以什么样(html或xhtml)的文档类型定义来解析文档,它必须声明在HTML文档的第一行。

34.手写一个instanceof方法?

function myInstanceof(target, origin) {
    //target instanceof origin,我们可以得知两点,首先target得是一个实例对象,origin得是一个构造器函数。
    if (typeof target !== 'object' || target == null) return false;  //typeof null === 'object'算是javascript中公认的设计缺陷了,因此就加了不等于null的判断了。
    if (typeof origin !== 'function') throw new TypeError('origin must be function');
    let proto = Object.getPrototypeOf(target); // 相当于proto = target.__proto__;
    while (proto) {
        if (proto === origin.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

35.手写一下如何实现数组扁平化?

concat函数是连接多个数组。

function flatten(arr) {
    return arr.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
};

36.手写一个redece算法?

function myReduce(cb, initValue) {
    const arr = this;
    let total = initValue || arr[0];
    for (let i = initValue ? 0 : 1; i < arr.length; i++) {
        total = cb(total, arr[i], i, arr); // 参数一,之前的汇总值  参数二,当前值  参数三,index, 参数四:数组本身
    } 
    return total;
}

37.手写实现js去重?

利用ES6的set关键字

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

利用ES5的filter方法

function unique(arr) {
  return arr.filter((item, index, array) => {
    return array.indexOf(item) === index;
  });
}

38.介绍一下内存泄漏?

答:内存泄漏就是指由于疏忽或者程序的某些错误造成未能释放已经不再使用的内存的情况,简单来讲就是,假设某个变量占用100M的内存,而你又用不到这个变量,但是这个变量没有被手动的回收或自动回收,即仍然占用100M的内存空间,这就是一种内存的浪费,即内存泄漏。

39.有哪些情况会造成内存泄漏?

1.闭包使用不当

该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子

<script>
    function fn1 () {
        let a = new Array(10000)  // 这里设置了一个很大的数组对象

        let b = 3

        function fn2() {
            let c = [1, 2, 3]
        }

        fn2()

        return a
    }

    let res = []  

    function myClick() {
        res.push(fn1())
    }
</script>

2.全局变量

此时这种情况就会在全局自动创建一个变量name,并将一个很大的数组赋值给name,又因为是全局变量,所以该内存空间就一直不会被释放

function fn1() {
    // 此处变量name未被声明
    name = new Array(99999999)
}

fn1()

3.分离的DOM节点

么叫DOM节点?假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放

<div id="root">
    <div class="child">我是子元素</div>
    <button>移除</button>
</div>
<script>

    let btn = document.querySelector('button')
    let child = document.querySelector('.child')
    let root = document.querySelector('#root')
    
    btn.addEventListener('click', function() {
        root.removeChild(child)
    })

</script>

4.控制台的打印

游览器一直会保存打印信息,并且不会清理。

<button>按钮</button>
<script>
    document.querySelector('button').addEventListener('click', function() {
        let obj = new Array(1000000)

        console.log(obj);
    })
</script>

5.遗忘的定时器

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj

<button>开启定时器</button>
<script>

    function fn1() {
        let largeObj = new Array(100000)

        setInterval(() => {
            let myObj = largeObj
        }, 1000)
    }

    document.querySelector('button').addEventListener('click', function() {
        fn1()
    })
</script>

40.typeof能正确区分原始值吗?

答:typeof是能正确区分出来原始值和对象类型的,原始值包括undefied、null、布尔值、数字和字符串。对象类型是数组、对象、函数等。 例如对于原始值undefined,typeof返回'undefined',对于原始值true,返回'boolean',对于原始值123,返回'number',对于原始值'hello',返回'string', 但是对于null的话,会返回object,属于js的遗留问题,对于null,最好使用===来判断。 typeof是不能区分对象类型的,比如[]和{}返回的都是object;

41.typeof的返回值有哪些?

答:

image.png

  • 'undefined':表示变量未定义和未赋值
  • 'boolean': 表示布尔类型
  • 'string': 表示字符串类型
  • 'number': 表示数字类型
  • 'object':[]和{}和null
  • 'function': 表示函数类型

42.typeof function会显示什么?

答:返回'function'

43.typeof为什么对null返回object,而不返回'null'?

答:在javaScript最初版本中,null被定义为一种对象类型,但是实际上它并不是一个对象,而是一个原始值,这导致typeof运算符无法正确地判断null的类型。

44.typeof('abc')和 typeof 'abc'都是 string, 那么 typeof 是操作符还是函数?

答:typeof是一个操作符,而不是一个函数,虽然他可以像函数一样使用,但它实际上是一个javaScript的内置一元操作符。

45.==的隐式转换规则是什么?

答:

1.如果两个操作数类型相同,则直接比较他们的值,若相等,则返回true. 2.若两个操作数类型不相同,则他们需要转换成相同的类型,再进行比较他们的值。

例如:其中一个操作数是布尔值,则将其转换为数字,true转换为1,false转换为0,然后再比较他们的值。

46.instaceof的原理是什么?

答:instanceof运算符只能用于检测对象和构建函数之间的关系,不能用于检测原始值得类型,比如:

var myString = "hello";
console.log(myString instanceof String); // 报错

上述代码会报错。

instanceof运算符是一种用来检测对象和构造函数之间关系的常用操作符,他的原理是通过检查对象的原型链中是否存在构建函数的原型来实现的。

47.["1","2","3"].map(parseInt) 结果是什么,并解释原因?

答: parseInt(string, radix)

string:需要转换的字符,如果不是字符串会被转换,忽视空格符。

radix:数字2-36之间的整形,默认使用10进制,这个参数的含义就是把前面字符看做多少进制的数字,所谓的基数。

["1", "2", "3"].map((value, index) => parseInt(value, index)) 所以

parseInt('1', 0); // 1 (parseInt的处理方式,这个地方item没有以"0x"或者"0X"开始,8和10这个基数由实现环境来定,ES5规定使用10来作为基数,因此这个0相当于传递了10)
parseInt('2', 1); // NaN (因为parseInt的定义,超出了radix的界限)
parseInt('3', 2); // NaN (虽然没有超出界限,但是二进制里面没有3,因此返回NaN)

如何让他返回[1, 2, 3]呢?

let numbers = ["1", "2", "3"].map(value => parseInt(value, 10));
console.log(numbers)

48.判断数据类型的方法?

答:

1.typeof可用于判断基本数据类型。(除了null)

2.instanceof用于判断对象的类型,返回值为布尔值。

3.Object.prototype.toString.call()用于判断数据类型,包括基本数据类型和引用数据类型。返回值是字符串。

4.Array.isArray(): 用于判断是否是数组,返回值为布尔值。

5.isNaN(): 用于判断是否为NaN,返回布尔值。

49. toFixed()会输出什么结果?

答:toFixed()是jS中Number类型的方法,用于将数字保留指定位数的小数并转换为字符串输出,会对数字进行四舍五入。

const num = 3.1415926;
console.log(num.toFixed(2)); // 输出 3.14
console.log(num.toFixed(4)); // 输出 3.1416

50.判断某个对象中是否包含某属性?

答:

1.in运算符:使用 in 操作符可以检查对象自身及其原型链中是否存在某个属性。

const obj = {name: "Tom", age: 18};
console.log("name" in obj) // 输出true

2.hasOwnProperty()方法,用于判断对象自身是否包含某个属性,返回值为布尔值。而不查找其原型链。

const obj = {name: "tome", age: 18};
console.log(obj.hasOwnProperty("name")); // 输出true

3.Object.keys()方法,用于获取对象的所有属性名,返回一个数组。

const obj = {name: "Tom", age: 18}
console.log(Object.keys(obj).includes("name")); // 输出true;

51.splice和slice你能说说有啥用和区别吗

答:splice()方法是用于在数组指定的位置删除和添加元素,可以实现以下操作。

const arr = ['a', 'b', 'c', 'd'];
arr.splice(1, 2); // 从第 1 个位置开始,删除 2 个元素,即删除 'b''c'
console.log(arr); // 输出 ['a', 'd']

arr.splice(1, 0, 'x', 'y'); // 从第 1 个位置开始,不删除元素,插入 'x''y'
console.log(arr); // 输出 ['a', 'x', 'y', 'd']

slice()是用于从数组中提出指定的元素,不会改变原数组,返回一个新数组,可以实现以下操作。

const arr = ['a', 'b', 'c', 'd'];
console.log(arr.slice(1, 3)); // 从第 1 个位置开始,到第 3 个位置结束,提取元素,即 ['b', 'c']
console.log(arr); // 输出 ['a', 'b', 'c', 'd'],原数组不变

52."=="和"==="的区别是什么?

答:

  • "=="运算符用于比较两个值是否相等,但是会进行类型转换,如果两个值类型不同,javascript会尝试将他们转换为相同的类型,然后进行比较。 例如:
console.log(1 == '1'); // 输出 true,因为 '1' 被转换为数字 1
console.log(true == 1); // 输出 true,因为 true 被转换为数字 1
console.log(null == undefined); // 输出 true,因为它们都是 falsy 值,被转换为 false

  • "==="运算符也用于比较两个值是否相等,不会进行类型转换,只有当两个值的类型和值完全相等时,才会返回true。例如:
console.log(1 === '1'); // 输出 false,因为它们的类型不同
console.log(true === 1); // 输出 false,因为它们的类型不同
console.log(null === undefined); // 输出 false,因为它们的类型不同

53.普通函数和构造函数的区别是什么?

答: 构造函数用于创建对象,通常使用大写字母开头的驼峰命名法,返回值默认为创建的对象,通过"new"关键词调用构造函数,会创建一个新的对象,并将该对象的原型指向构造函数的"prototype"属性。普通函数没有原型属性,不能用于创建对象,但可以作为对象的方法使用。

54.类数组与数组区别,为什么要设置类数组?

答:类数组是指具有数组特性,例如length属性和可迭代性,但不是数组实例的对象。

类数组和数组的主要区别如下:

1.类数组中没有数组的实例方法,比如'push()'、'pop()'等方法。

2.类数组可以使用下标访问元素,因为它们具有类似数组的结构。

const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
console.log(arrayLike[0]); // 输出 'a'
console.log(arrayLike[1]); // 输出 'b'
console.log(arrayLike[2]); // 输出 'c'

设置类数组的主要目的是实现类似数组的操作,同时避免创建数组实例带来的性能开销。

55.null和undefined的区别是什么?

JavaScript 的最初版本是这样区分的:

null 是一个表示"无"的对象(空对象指针),转为数值时为 0

典型用法是:

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

undefined 是一个表示"无"的原始值,转为数值时为 NaN

典型用法是:

  • 变量被声明了,但没有赋值时,就等于 undefined

  • 调用函数时,应该提供的参数没有提供,该参数等于 undefined

  • 对象没有赋值的属性,该属性的值为 undefined

  • 函数没有返回值时,默认返回 undefined

56.介绍一下this的指向?

1.全局环境下,this指向全局对象。

console.log(this); // window (在浏览器中)

2.在函数中,this指向调用该函数的对象,如果没有明确的调用对象,则this指向全局对象。

function foo() {
  console.log(this);
}
foo(); // window (在浏览器中)

var obj = {
  name: 'Tom',
  sayName: function() {
    console.log(this.name);
  }
}
obj.sayName(); // Tom

3.在对象方法中,this指向该对象本身

var obj = {
  name: 'Tom',
  sayName: function() {
    console.log(this.name);
  }
}
obj.sayName(); // Tom

4.在构造函数中,this指向正在创建的实例对象。

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  }
}
var tom = new Person('Tom');
tom.sayName(); // Tom

5.在箭头函数中,this的指向与定义该函数时所在的上下文环境相同,与调用时的上下文环境无关。箭头函数的this指向,是父级程序的this指向,下面的箭头函数没有父级程序,所以this指向window(下面例子中对象是没有this的,箭头函数this指向是window)

var obj = {
  name: 'Tom',
  sayName: () => {
    console.log(this);
  }
}
obj.sayName(); // window

57.介绍一下原型和原型链?

答:在JavaScript中,每个对象都有一个原型对象(prototype),原型对象又可以有自己的原型对象,形成一个链式结构,称为原型链(prototype chain)。 JS中的原型链用于实现继承和属性查找,当我们访问一个对象的属性时或方法时,如果该对象本身没有该属性或方法,JS会沿着原型链向上查找,直到找到为止。

58.同步和异步的执行顺序是怎么样的?(你能讲一下事件循环机制吗?)

答:

1.JavaScript将任务分为同步任务和异步任务,同步任务进入主线中,异步任务首先到Event Table进行回调函数注册。

2.当异步任务的触发条件满足,将回调函数从Event Table压入到Event Queue中。

3.主线程里面的同步任务执行完毕,系统会去Event Queue中读取异步的回调函数。

4.只要主线程空了,就会去Event Queue读取回调函数,这个过程被称为Event Loop。

举例:

  • setTimeout(cb, 1000),当1000ms后,就将cb压入Event Queue。
  • ajax(请求条件, cb),当http请求发送成功后,cb压入Event Queue(事件循环机制)。

image.png

59.Event Loop的执行过程是怎么样的?(宏任务和微任务的执行顺序)

答:

1.代码开始执行,创建一个全局调用栈,script作为宏任务执行。

2.执行过程同步任务立刻执行,异步任务根据任务类型分别注册到微任务队列和宏任务队列。

3.同步任务执行完毕,查看微任务队列。

  • 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)

  • 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直到宏任务队列为空。

image.png

image.png

60.介绍一下JS中的作用域和作用域链?

答:

作用域是指变量或函数可访问的范围。在JavaScript中,作用域分为全局作用域和函数作用域。全局作用域中定义的变量和函数是可以在任何地方被访问,而函数作用域中定义的变量和函数只能在函数内部被访问。

作用域链是指JS在查找变量和函数时所采用的一种机制,当JS在当前作用域中查找变量或函数时,如果找不到,就会沿着作用域链向上查找,直到找到为止。作用域链的顶端是全局作用域,而每个函数都有自己的作用域链。

61.谈谈对闭包的理解?

答:闭包是指一个函数能够访问其作用域外部的变量,并且在函数内部能够持续访问这些变量,简单来说,闭包就是一个函数和其相关的变量的集合体。 在javascript中,当一个函数内部定义了另一个函数,且内部函数引用了外部函数的变量时,就会形成一个闭包。这个内部函数可以持续访问外部函数的变量,并且这些变量的值在函数执行完毕以后仍然存在。

62.介绍一下new操作符做了哪些事情?

答:

1.创建一个新对象,并将这个对象的_proto_属性指向构造函数的prototype属性。

2.将构造函数的this指向新对象。

3.执行构造函数中的代码,并将新对象作为函数的上下文调用。

4.如果构造函数返回一个对象,那么返回这个对象,否则返回新对象。

63.ES6推出了哪些新特性?

1.支持let与const

在之前JS是没有块级作用域的,const与let填补了这方面的空白,const与let都是块级作用域,都必须在声明之后使用。在声明之前使用的话,会报ReferenceError的错误。

2.Promise的引入

在ES6引入Promise作为异步编程的一种解决方案,比传统的回调函数的异步方式更加方便,解决了回调地狱问题。

3.拓展运算符的引入(...)

拓展运算符...可以在函数调用/数组构造的时,将数组表达式或string在语法层面展开,还可以在构造对象的时候,将对象表达式按照key-value的方式展开。

// 函数调用:
myFunction(...iterableObj);
// 数组构造或字符串:
[...iterableObj, '4', ...'hello', 6];
构造对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性):
let objClone = { ...obj };
4.解构赋值的引入

解构赋值语法是JavaScript的一种表达式,可以方便的从数组或者对象中快速提取值赋给定义的变量。

获取数组的值:

var foo = ["one", "two", "three", "four"];

var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

//如果你要忽略某些值,你可以按照下面的写法获取你想要的值
var [first, , , last] = foo;
console.log(first); // "one"
console.log(last); // "four"

//你也可以这样写
var a, b; //先声明变量

[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2

获取对象的值:

const student = {
  name:'Ming',
  age:'18',
  city:'Shanghai'  
};

const {name,age,city} = student;
console.log(name); // "Ming"
console.log(age); // "18"
console.log(city); // "Shanghai"
5. 模板字符串的引入

ES6支持模板字符串,使得字符串的拼接更加的简洁、直观。

不使用模板字符串:

var name = 'Your name is ' + first + ' ' + last + '.'

使用模板字符串:

var name = `Your name is ${first} ${last}.`

在ES6中通过${}就可以完成字符串的拼接,只需要将变量放在大括号之中。

6.箭头(Arrow)函数的引入

箭头函数的引入,解决了this指向的问题,箭头函数的this是捕获其上层作用域的this,在定义时已经确定,不能通过apply、bind、call进行修改。

7.class(类)的引入

通过引入了类的概念,让js的面向对象编程变得更加简单。实现继承更加方便。

8.模块化import和export的引入
  • 静态分析: ES6 的模块化使用静态分析机制。这意味着在编译时就能够确定所有模块的依赖关系,而不需要在运行时解析代码。这使得模块加载更快,并且能够进行更好的优化。
  • ES6中导入的值是导出值的引用,改变一个会影响到另一个。这种引用关系使得模块之间能够共享数据,并能够在不同模块之间实现数据的传递和共享。对于导出的值是基本数据类型(如数字、字符串、布尔值等)时,导入模块中的变量与导出模块中的值是相互独立的,改变其中一个不会影响到另一个。

64.介绍一下防抖和节流?

答:

防抖: 在一定时间内,多次触发同一时间,只执行最后一次。也就是说,如果在规定时间内再次触发该时间,就会清除之前的计时器,重新开始计时。防抖常用于搜索框输入查询等场景,可以减少请求次数,提高性能。

节流: 在一定时间内,无论触发了多少次事件,只执行一次,也就是说,如果在规定时间内再次触发该事件,不会重新计时,而是直接忽略该次事件,节流常用于页面滚动、鼠标移动等场景。 总之,防抖是为了避免频繁触发事件,而节流是为了避免事件处理过于频繁。

65.script标签的defer和async属性有什么区别?

答:defer和async属性都可以异步加载脚本,提高页面加载速度,但defer保证脚本的执行顺序,适用于需要按照顺序加载的脚本,而async则不能保证执行顺序,适用于独立的脚本。

65.使用new调用函数,而这个函数中有return,那么它return出来的是什么?

答:构造函数有返回值情况,若构造函数内返回值为对象,则直接返回这个对象。 否则返回new创建的对象。

例子: 返回值为基本类型,没有受到任何干扰。

function Thin_User(name, age) {
    this.name = name;
    this.age = age;
    return 'i will keep thin forever';
}

Thin_User.prototype.eatToMuch = function () {
    console.log('i eat so much, but i\'m very thin!!!');
}

Thin_User.prototype.isThin = true;

const xiaobao = new Thin_User('zcxiaobao', 18);
console.log(xiaobao.name);   // zcxiaobao
console.log(xiaobao.age);    // 18
console.log(xiaobao.isThin); // true
// i eat so much, but i'm very thin!!!
xiaobao.eatToMuch(); 

返回值为对象,直接返回的我们return的对象

function Thin_User(name, age) {
    this.name = name;
    this.age = age;
    return {
        name: name,
        age: age * 10,
        fat: true
    }
}

Thin_User.prototype.eatToMuch = function () {
    // 白日做梦吧,留下肥胖的泪水
    console.log('i eat so much, but i\'m very thin!!!');
}

Thin_User.prototype.isThin = true;

const xiaobao = new Thin_User('zcxiaobao', 18);
// Error: xiaobao.eatToMuch is not a function
xiaobao.eatToMuch();

66.setTimeout、Promise、Async/Await的区别是什么?

答: 事件循环中分为宏任务队列和微任务队列。 其中setTimeout的回调函数放到宏任务队列中,promise.then里的回调函数会放到相应微任务队列中,async函数表示函数里面可能会有异步方法,await后面跟着一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列中。

67.Object的__proto__指向什么?(Function的__proto__指向什么)

答:Object是构造函数,所有的函数都是通过new Function创建,因此Object相当于Function的实例,即Object.proto-->Function.prototype

Function函数不通过任何东西创建,JS引擎启动时,添加到内存中,Function.proto --> Function.prototype

67.js文件为什么要放在文件底部?

答:游览器在解析HTML页面时,会按照从上到下的顺序逐行解析,如果将javascript文件放在头部,游览器在下载javaScript文件时,会阻塞HTML页面的解析和渲染,导致页面加载缓慢。

68.什么是promise?实现一个简单的promise?

答:Promise是ES6中的异步编程解决方案,用于处理异步操作。它可以将异步操作的结果以同步的方式返回,避免回调地狱问题,使代码更加简洁易读。

function MyPromise(fn) {
  var self = this;
  self.value = null; // 用于存储异步操作成功时的结果值
  self.error = null; // 用于存储异步操作失败时的错误信息
  self.onFulfilled = null; // 用于存储异步操作成功时的回调函数
  self.onRejected = null; // 用于存储异步操作失败时的回调函数

  function resolve(value) {
    if (self.onFulfilled) {
      self.onFulfilled(value); // 调用异步操作成功时的回调函数,并传入结果值
    }
  }

  function reject(error) {
    if (self.onRejected) {
      self.onRejected(error); // 调用异步操作失败时的回调函数,并传入错误信息
    }
  }

  fn(resolve, reject); // 执行异步操作,并传入 resolve 和 reject 函数
}

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  var self = this;
  self.onFulfilled = onFulfilled; // 注册异步操作成功时的回调函数
  self.onRejected = onRejected; // 注册异步操作失败时的回调函数
};

const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        console.log("执行成功");
        resolve("成功");
    }, 5000);
});
promise.then((data) =>{
    console.log(data)
})

69.Promise.all(), .race(), .allSettled()对比?

1.Promise.all()方法接受一个Promise对象数组作为参数,当数组中所有Promise对象都成功时,返回一个包含所有Promise对象结果的数组,如果有任何一个Promise对象失败,返回该对象的错误信息。

2.Promise.race()方法接受一个Promise对象数组作为参数,但只返回最先完成的异步操作的结果,可以用于设置超时时间等场景。

3.Promise.allSettled()方法接受一个Promise对象作为参数,当数组中所有Promise对象都完成时,返回一个包含所有Promise对象结果的数组,无论Promise对象

70.Promise、Generator、Async三者的区别?

答:

Promise是ES6中异步编程解决方案,用于处理异步操作。他可以将异步操作结果以同步的方法返回,避免回调地狱,使代码更加简洁。

Generator是ES6中的一种特殊函数,可以暂停执行并在需要时恢复执行。通过yield关键字可以将函数的执行权交给调用者,调用者可以在需要时恢复执行。从而实现异步操作。

Async是ES7中的异步编程解决方案,基于Generator实现,可以方便处理异步操作。Async函数是异步函数的一种特殊形式,可以使用await关键字等待异步操作的结果。

71.async和await的实现原理是什么?

答:async和await的实现原理是基于Promise对象的特性,async函数内部返回一个Promise对象,await表达式会暂停async函数的执行,直到Promise对象返回结果。async函数可以使用Promise对象的then方法获取异步操作结果。

72.let const var的区别是什么?

答: var-ES5变量声明方式

  • var可以在声明之前使用,
  • 作用域var的作用域为方法作用域,只要在方法内定义了,整个方法都可以用。

let-ES6变量声明方式

  • 在变量声明前使用会报错。
  • let为块级作用域,也就是{}
  • let禁止重复声明,var允许重复声明

const-ES6常量声明方式

  • const为常量声明方式,声明时必须初始化赋值,后续不得修改该值。

73.箭头函数和普通函数的区别?

答:

  1. 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this,而非执行时。普通函数this是执行时动态绑定的。
  2. 箭头函数不可作为构造函数,不能使用new命令。
  3. 箭头函数没有arguments对象,如果要用,使用rest参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  5. 不能用call/bind/apply修改this指向,但是可以通过修改外层作用域this来间接修改。
  6. 箭头函数没有prototype属性。

74.Map与WeakMap的区别?(Set和WeakSet的区别?)

答:Map的键名可以是任意类型,包括基本类型和对象类型,而WeakMap的键名只能是引用类型,Map中键或值没有其他引用时,不会被垃圾回收,而WeakMap只要键没有其他引用,就会被垃圾回收。Map有keys()、values()、entries()方法的迭代器,而WeakMap没有迭代器。

75.map和object的区别?

答:object的属性值只能是字符串和Symbol类型。而map的键值可以是基本类型和对象类型。object只能通过遍历获取键值对数量,而map可以直接通过size属性。Object适合存储简单的键值对,不需要使用迭代器的场合,而Map使用存储复杂的键值对,需要使用迭代器和获取键值对数量的场合。

76.'1'.toString()为什么可以调用?

答:

var s = new Object('1');
s.toString();
s = null;

第一步创建Object实例 第二步调用实例方法。 第三步执行完方法立即销毁这个实例。

77.0.1+0.2为什么不等于0.3?

答:0.1和0.2在转换二进制后,会出现无限循环,由于标准位数的限制,后面多余的位数会被截掉,此时会出现精度损失,从而导致不相等。

78.Object.assign是深拷贝还是深拷贝?实现深拷贝的方法有哪些?

答:Object.assign是浅拷贝,是将源对象的可枚举属性复制到目标对象中,源对象中存在同名属性,后面的覆盖前面。且只拷贝对象的一层属性,如果属性的值是引用类型,拷贝的是引用,原对象和新对象会共享同一个引用类型。

实现深拷贝有三种方法:

  1. 使用递归拷贝来实现:
function deepClone(source) {
    let target;
    if (typeof source !== 'object' || source === null) {
        return target;
    }
    // 区分source是数组类型还是对象类型。
    if (Object.prototype.toString.call(source) === '[object Array]') {
        target = [];
    } else {
        target = {};
    }
    for (let key in source) {
        // 如果不是对象的话,直接赋值
        if (typeof source[key] !== 'object') {
            target[key] = source[key];
        } else {
            // 如果是对象,要递归遍历
            target[key] = deepClone(source[key]);
        }
    }
    return target;
}

  1. 使用JSON序列化和反序列化来实现:
const obj1 = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4],
  },
};
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 5;
console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 5

  1. 使用lodash 的 cloneDeep() 方法

79.介绍一下事件冒泡和事件捕获?

答:

事件冒泡是由微软提出的事件流,事件冒泡可以形象的比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面,也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。

事件捕获是由网景公司提出的,与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

80.什么是事件委托?

答:

事件委托是一种设计模式,通过将事件绑定到父元素上,利用事件冒泡机制来处理子元素触发的事件,在事件委托中,不需要为每个子元素单独绑定事件,而是将事件处理程序绑定到他们的共同父元素上。

事件委托的原理是当子元素触发事件时,事件会沿着DOM树向上冒泡,最终到达父元素,父元素上绑定的事件处理程序会捕获到这个事件,并通过事件对象的target属性来判断是哪个子元素触发了事件,然后根据子元素的标识或其他属性来执行相应的逻辑。

事件委托的好处是减少事件处理程序的数量,提高性能和内存利用率,特别是在有大量子元素或动态添加子元素的情况下,使用事件委托可以简化代码并减少内存消耗。

案例:

// HTML 结构
<ul id="parent">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

// JavaScript 代码
const parent = document.getElementById('parent');

parent.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log(event.target.textContent);
  }
});

81.获取元素在页面位置的API有哪些?

  • getBoundingClientRect():该方法返回一个 DOMRect 对象,包含了元素的位置信息,包括 left、top、right、bottom、width 和 height。
  • offsetLeft和offsetTop:这两个属性返回元素相对于其最近的具有定位(position 属性为 relative、absolute 或 fixed)的父元素的左上角的偏移量。
  • offsetParent:该属性返回元素的最近的具有定位属性的父元素。
  • scrollTop 和 scrollLeft:这两个属性返回或设置元素的滚动距离,可以通过这些值来计算元素相对于页面的位置。
  • getComputedStyle():该方法返回一个包含元素所有计算后样式的对象,可以通过计算后样式的 top 和 left 值来获取元素的实际位置。

82.在 timer = setInterval(() => {}, delay) 中,这个 timer 打印出来是什么?

答:此时返回值intervalID是一个非零数值,用来标识通过setInterval()创建的计时器,这个值可以用来作为clearInterval()的参数来清除对应的计时器。

82.for in 和 Object.keys区别?

答:for...in循环遍历对象的键,可以遍历对象自身的可枚举属性和继承的可枚举属性。 Object.keys()返回对象自身的可枚举属性组成的数组,不包括继承的属性。 举例:

var obj = {
  a: 1,
  b: 2,
};

Object.prototype.c = 3;

for (var key in obj) {
  console.log(key); // 输出 ab、c
}

var keys = Object.keys(obj);
console.log(keys); // 输出 ["a", "b"]

83.script标签中defer和async的区别?

答:如果没有defer和async属性,游览器会加载并执行相应的脚本,他不会等待后续加载的文档元素,读取到以后就会开始加载和执行,这样会阻塞了后续文档的加载。defer和async属性都是异步加载外部js脚本文件,他不会阻塞页面的解析,区别如下:

  1. 多个async属性不保证加载的顺序,多个带defer属性的标签,按照加载顺序执行。
  2. async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行执行的。而defer属性加载后续文档的过程(此时是仅加载但没有执行)是并行进行的,但是js脚本的执行需要等待文档所有元素解析完成以后才执行。

84.HTML5有哪些更新?

答:

总结: (1)新增语义化标签:nav、header、footer、aside、section、article (2)音频、视频标签:audio、video (3)数据存储:localStorage、sessionStorage (4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议) (5)input标签新增属性:placeholder、autocomplete、autofocus、required (6)history API:go、forward、back、pushstate

85.行内元素有哪些?块级元素有哪些?空(void)元素有哪些?

答:

  • 行内元素有:a b span img input select strong

  • 块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p

空元素,即没有内容的HTML元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:

  • 常见的有:<br><hr><img><input><link><meta>

  • 鲜见的有:<area><base><col><colgroup><command><embed><keygen><param><source><track><wbr>

86.介绍一下web worker?

答:Web Worker是一种在浏览器中运行的JavaScript脚本,可以在后台线程中执行任务,而不会阻塞用户界面的操作,他们可以用于处理一些耗时的计算、网络请求和其他复杂的操作,以提高网页的性能和响应性。 Web Worker可以在主线程之外创建和运行,它们与主线程之间通过消息传递进行通信,主线程可以向Worker发送消息,并接收Worker返回的结果。这种消息传递是异步的,不会阻塞主线程的执行,Worker可以在后台进行计算或处理,然后结果发送回主线程进行处理。

87.title与h1的区别,b与strong的区别,i与em的区别?

  • title属性没有明确意义只表示是个标题,H1则表示层次明确的标题,对信息页面的抓取有很大的影响。
  • strong标签有语义,起到加重语气的效果,而b标签没有的,仅仅是个加粗标签。
  • i内容展示为斜体,em表示强调的文本。

88.Canvas和SVG的区别?

  • SVG是可缩放矢量图形,基于可拓展标记语言XML描述的2D图形的语言,SVG使用矢量图形的方式,它使用XML描述图形,可以无损地缩放和变换图形。且可以通过修改XML代码动态修改图形。

  • Canvas是画布,通过JS来绘制2D图形,是逐像素进行渲染的,其位置发生改变,就会重新进行绘制。当缩放Canvas图形时,可能会出现失真。

89.介绍一下CSS选择器及其优先级?

答:!important > 内联样式 > ID选择器 > 类选择器/伪类选择器/属性选择器 > 标签选择器/伪元素选择器>兄弟选择器/子选择器/后代选择器/通用选择器(权重为0)

90.display的属性值及其作用?

image.png

91.display的block、inline和inline-block的区别是什么?

答:

  • block元素会独占一行,宽度默认为100%,可以设置宽度、高度、外边距和内边距。
  • inline元素不独占一行,宽度根据内容自适应,不能设置宽度、高度、外边距和内边距。
  • inline-block元素不独占一行,宽度根据内容自适应,可以设置宽度,高度,外边距和内边距。

92.隐藏元素的方法有哪些?介绍一下?

答:

  • display:none:渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件。
  • visibility: hidden:元素在页面中仍然占据位置,但是不会响应绑定的监听事件。
  • opacity:0 :将元素的透明度设置为0,以此来实现元素的隐藏,元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件。
  • position:absolute: 通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏。
  • z-index:负值: 来使用其他元素遮盖住该元素,以此来实现隐藏。
  • transform:scale(0,0):将元素缩放为0,来实现元素的隐藏,这种方法中,元素仍在页面中占据位置,但是不会响应绑定的监听事件。

94.link和@import的区别是什么?

答:

  • link标签在HTML文档的<head>部分使用,页面加载时同时加载外部CSS样式表,几乎可以兼容所有游览器,该标签引入的具有较高优先级,可以覆盖其他样式表。

  • @import规则在CSS文件内部使用,页面加载完毕后才加载外部CSS样式表,优先级低。

95.transition和animation的区别?

答:

  • transition是过度属性,他的实现需要触发一个事件(比如鼠标移动上去,焦点,点击等)才执行动画。
  • animation是动画属性,它的实现不需要触发事件,设定好时间之后可以自己执行,且可以循环一个动画。

96.display:none与visibility:hindden的区别?

答“”这两个属性都是让元素隐藏,不可见,区别如下:

  • display:none会让元素完全从渲染树中消失,渲染时不会占据任何空间,且display:none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点属性也无法显示。修改display会导致文档重排。
  • visibility:hidden不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。且是继承属性,可以通过修改子孙的visibility:visible让子孙显示出来。修改visibility属性会导致元素的重绘。

97.伪类和伪元素的区别和作用是什么?

答:

  • 伪元素是在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}

  • 伪类是将特殊的效果添加到特定的选择器上,它是改变已有元素的状态,不会产生新的元素。
a:hover {color: #FF00FF}
p:first-child {color: red}

98.对requestAnimationframe的理解?

答:window.requestAnimationFrame()告诉浏览器,你希望执行一个动画,并且要求游览器在下次重绘之前调用指定的回调函数更新动画,该方法需要传入一个回调函数作为参数,该回调函数会在游览器下一次重绘之前执行。requestAnimationFrame()在页面处于不可见状态时,该页面的屏幕刷新任务也会被系统暂停,有效节省cpu开销。且requestAnimationFrame会把每一帧中所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。

优势:

  • CPU节能:使用SetTinterval 实现的动画,当页面被隐藏或最小化时,SetTinterval 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而RequestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统走的RequestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
  • 函数节流:在高频率事件( resize, scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,RequestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销,一个刷新间隔内函数执行多次时没有意义的,因为多数显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。
  • 减少DOM操作:requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。

setTimeout执行动画的缺点:它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是:

  • settimeout任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;

  • settimeout的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。

99.对盒模型的理解

image.png

image.png

  • 标准盒模型(默认值)的width和height属性的范围只包含了content。设置box-sizeing:content-box
  • IE盒模型(怪异盒模型)的width和height属性的范围包含了border、padding和content。设置box-sizeing:border-box

100.为什么有时候用tanslate来改变位置而不是定位?

答: 改变元素的tanslate不会触发重排或重绘,只会触发复合,使用元素改变元素定位的方式可能会触发重排,所以使用tanslate改变位置更加顺滑。transform使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此translate()更⾼效,可以缩短平滑动画的绘制时间。 ⽽translate改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。

101.li与li之间有看不见的空白间隔是什么原因引起的?如何解决?

答:浏览器会把inline内联元素间空白字符(空格、换行、Tab等)渲染成一个空格,为了美观,通常是一个<li>放在一行,这导致<li>换行后产生换行字符,它就会变成一个空格,占用了一个字符的宽度。 解决办法:

  • <li>设置float:left
  • 将所有<li>写在同一行
  • 消除<ul>的字符间隔letter-spacing: -8px

102.CSS3中有哪些新特性?

答:

  • 新增各种CSS选择器(:not(.input): 所有class不是"input"的结点)
  • 圆角(border-radius:8px)
  • 多列布局(multi-column layout)
  • 阴影和反射(Shadow/reflection)
  • 文字特效(text-shadow)
  • 文字渲染(text-decoration)
  • 线性渐变(gradient)
  • 旋转(transform)
  • 增加了旋转,缩放,定位,倾斜,动画,多背景

103.对CSS Sprites的理解?

答: CSS Sprites是一种优化网页加载速度和降低服务器负载的技术,它通过将多个小的图像合并成一张大的图像,然后使用CSS(background-image,background-repeat,background-position)来定义每个小图像在页面中的位置和尺寸,这样一来,只需要加载一张大图像,而不是多个小图像,从而减少了HTTP请求的数量,提高了页面加载速度。

104.什么是物理像素、逻辑像素和像素密度?

答:

  • 物理像素(Physical Pixel)是指显示设备的最小可见单元,它构成显示屏的物理元素。
  • 逻辑像素(Logical Pixel)是指网页或应用程序中使用的抽象像素单位。它不直接映射到物理像素,而根据设备的屏幕密度和缩放比例进行转换。
  • 像素密度(Pixel Density)是指在给定物理尺寸下,显示设备上每厘米的物理像素的数量。

105.margin和padding的使用场景有哪些?

答:

  • 需要在border外侧添加空白,且空白处不需要背景色时,使用margin
  • 需要在border内侧添加空白,且空白处不需要背景色时,使用padding

106.对line-height的理解及其赋值方式?

答: (1)line-height的概念:

  • line-height 指一行文本的高度,包含了字间距,实际上是下一行基线到上一行基线距离;
  • 如果一个标签没有定义 height 属性,那么其最终表现的高度由 line-height 决定;
  • 一个容器没有设置高度,那么撑开容器高度的是 line-height,而不是容器内的文本内容;
  • 把 line-height 值设置为 height 一样大小的值可以实现单行文字的垂直居中;
  • line-height 和 height 都能撑开一个高度;

(2)line-height 的赋值方式:

  • 带单位:px 是固定值,而 em 会参考父元素 font-size 值计算自身的行高
  • 纯数字:会把比例传递给后代。例如,父级行高为 1.5,子元素字体为 18px,则子元素行高为 1.5 * 18 = 27px
  • 百分比:将计算后的值传递给后代

image.png

107.CSS优化和提高性能的方法有哪些?

答:

  • css压缩:将写好的css进行打包压缩,可以减小文件体积。
  • 减少重绘和重排:避免频繁的修改DOM元素样式和结构,减少游览器的重绘和重排,提高性能。
  • 减少使用@import,建议使用link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。
  • 使用CSS Sprites:将多个小图标合并成一个大的图片,通过background-position来显示不同的图标,减少http请求。

108.display:inline-block什么时候会显示间隙?

答:当使用display:inline-block时,如果在HTML结构中,元素之间存在空格,换行符等,这些空白字符会被解析成空间,导致在元素之间显示间隙。 解决办法:

  • 去掉空白字符,将元素紧密放置在一起。
  • 使用负的'margin'来消除间隙。
  • 将元素的父容器的font-size设置为0;

109.如何写一个单行和多行文本溢出隐藏?

答:

  • 单行文本溢出
overflow:hidden;
text-overflow:elliipsis;
white-space:nowrap;
  • 多行文本溢出
overflow:hidden;
text-overflow:ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:3;

使用"-webkit-"前缀可以让开发者针对仅在WebKit浏览器中支持的特定CSS属性进行样式设置,以确保在这些浏览器中正确显示和渲染。

110.Sass、Less是什么?为什么要使用它们?

答:他们都是CSS预处理器,是CSS上的一种抽象层。Sass(Syntactically Awesome Stylesheets)和Less(Leaner Style Sheets)都允许开发者使用类似编程语言的语法来编写CSS样式。它们提供了一些功能和特性,如变量、嵌套规则、函数、混合(Mixins)等,使得CSS代码更易于编写、维护和扩展。

111.对媒体查询的理解?

答:媒体查询是指在Web开发中使用CSS样式表针对不同媒体设备或条件应用不同的样式,通过媒体查询,可以根据设备的屏幕大小、分辨率、浏览器类型来调整页面的布局和样式,以提供更好的用户体验。

<!-- link元素中的CSS媒体查询 --> 
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> 
<!-- 样式表中的CSS媒体查询 --> 
<style> 
@media (max-width: 600px) { 
  .facet_sidebar { 
    display: none; 
  } 
}
</style>

112.如何判断元素是否到达可视区域?

  • window.innerHeight是浏览器可视区的高度
  • document.body.scrollTop || document.documentElement.scrollTop是浏览器滚动过的距离。
  • imgs.offsetTop是元素顶部距离文档顶部的高度(包括滚动条的距离) 所以如果满足img.offsetTop < window.innerHeight + documnet.body.scrollTop的话,则表示元素是处于可视区域。

image.png

113.z-index属性在什么情况下会失效?

答:通常z-index的使用是在有两个重叠的标签,在一定的情况下控制其中一个在另一个的上方或者下方出现。 z-index值越大,就越在上层。z-index元素的position属性需要是relative、absolute或fixed。 z-index会在以下情况失效:

  • 父元素position为relative时,子元素z-index失效。解决:父元素position设置为absolute
  • 元素没有设置position属性,解决:设置该元素的position属性为relative、absolute或fixed中一种。
  • 元素在设置z-index的同时还设置了float浮动。解决:去掉float。改为display:inline-block.

113.CSS3中的tansform有哪些属性?

答:Css3中的transform有以下几个属性:

  • translate:用于平移元素,在水平和垂直方向上移动元素。
  • rotate:用于旋转元素。
  • scale:用于缩放元素。
  • skew:用于倾斜元素。
  • matrix:用于同时进行平移、旋转、缩放、和倾斜操作。

113. 常见的CSS布局单位

常用的布局单位包括像素(px),百分比(%),emremvw/vh

(1)像素px)是页面布局的基础,一个像素表示终端(电脑、手机、平板等)屏幕所能显示的最小的区域,像素分为两种类型:CSS像素和物理像素:

  • CSS像素:为web开发者提供,在CSS中使用的一个抽象单位;
  • 物理像素:只与设备的硬件密度有关,任何设备的物理像素都是固定的。

(2)百分比%),当浏览器的宽度或者高度发生变化时,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。

(3)em和rem相对于px更具灵活性,它们都是相对长度单位,它们之间的区别:em相对于父元素,rem相对于根元素。

  • em: 文本相对长度单位。相对于当前对象内文本的字体尺寸。如果当前行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(默认16px)。(相对父元素的字体大小倍数)。
  • rem: rem是CSS3新增的一个相对单位,相对于根元素(html元素)的font-size的倍数。作用:利用rem可以实现简单的响应式布局,可以利用html元素中字体的大小与屏幕间的比值来设置font-size的值,以此实现当屏幕分辨率变化时让元素也随之变化。

(4)vw/vh是与视图窗口有关的单位,vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度,除了vw和vh外,还有vmin和vmax两个相关的单位。

  • vw:相对于视窗的宽度,视窗宽度是100vw;
  • vh:相对于视窗的高度,视窗高度是100vh;
  • vmin:vw和vh中的较小值;
  • vmax:vw和vh中的较大值;

vw/vh 和百分比很类似,两者的区别:

  • 百分比(%):大部分相对于祖先元素,也有相对于自身的情况比如(border-radius、translate等)

  • vw/vm:相对于视窗的尺寸

114.说一下px、em、rem的区别及使用场景?

答:

  • px是固定的像素,一旦设置就无法因为适应页面大小而改变。主要用于只需要适配少量移动设备的场景。
  • em是一个相对于父元素的字体的大小单位,主要用于相对于父元素调整尺寸的效果。
  • rem是一个相对于页面根元素字体大小的单位,默认是1rem等于16px,主要用于适配分辨率差别较大的设备。

115.手写一下两栏布局的实现?

答: 原理: 一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,利用浮动,将左边元素宽度设置为200px,并且设置向左浮动,将右边元素的margin-left设置为200px(可以保证在同一行),宽度设置为auto(默认为auto,撑满整个父元素)

<style>
    .outer {
        height: 100px;
    }
    .left {
        float: left;
        width: 200px;
        background: aqua;
    }
    .right {
        margin-left: 200px;
        width: auto;
        background: brown;
    }
</style>

116.手写一下三栏布局的实现?

答: 方法一:

原理: 利用浮动,左右两栏设置固定大小,并设置对应方向的浮动,中间一栏设置左右两个方向的margin值,注意:中间一栏必须放在最后

<style>
   .outer {
       height: 200px;
   }
   .left {
       float: left;
       width: 200px;
        height: 200px;
       background: brown;
   }
   .right {
       float: right;
       width: 200px;
       height: 200px;
       background: aqua;
   }
   .middle {
       width: auto;
       height: 200px;
       margin-left: 200px;
       margin-right: 200px;
       background: blue;
   }
</style>

方法二:

  • 利用绝对定位,左右两栏设置为绝对定位,中间设置对应方向大小的margin的值。
<style>
    .outer {
        position: relative;
        height: 100px;
    }

    .left {
        position: absolute;
        width: 100px;
        height: 100px;
        background: tomato;
    }

    .right {
        position: absolute;
        top: 0;
        right: 0;
        width: 200px;
        height: 100px;
        background: gold;
    }

    .center {
        margin-left: 100px;
        margin-right: 200px;
        height: 100px;
        background: lightgreen;
    }
</style>

方法三:

  • 利用flex布局,左右两栏设置固定大小,中间一栏设置为flex:1。
.outer {
  display: flex;
  height: 100px;
}

.left {
  width: 100px;
  background: tomato;
}

.right {
  width: 100px;
  background: gold;
}

.center {
  flex: 1;
  background: lightgreen;
}

方法四:

  • 圣杯布局,利用浮动和负边距来实现。父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。
.outer {
    height: 100px;
    padding-left: 100px;
    padding-right: 200px;
}

.left {
    position: relative;
    left: -100px;

    float: left;
    margin-left: -100%;

    width: 100px;
    height: 100px;
    background: tomato;
}

.right {
    position: relative;
    left: 200px;

    float: right;
    margin-left: -200px;

    width: 200px;
    height: 100px;
    background: gold;
}

.center {
    float: left;
    
    width: 100%;
    height: 100px;
    background: lightgreen;
}

方法五:

方法四:

  • 双飞翼布局,双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。
.outer {
  height: 100px;
}

.left {
  float: left;
  margin-left: -100%;

  width: 100px;
  height: 100px;
  background: tomato;
}

.right {
  float: left;
  margin-left: -200px;

  width: 200px;
  height: 100px;
  background: gold;
}

.wrapper {
  float: left;

  width: 100%;
  height: 100px;
  background: lightgreen;
}

.center {
  margin-left: 100px;
  margin-right: 200px;
  height: 100px;
}

117.手写一下水平垂直居中的实现?

答:

方法一:利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后在通过translate来调整元素中心点到页面中心。

<style>
   .parent {
       position: relative;
       height: 200px;
       width: 100px;
       background: aqua;
   }
   .child {
       position: absolute;
       top: 50%;
       left: 50%;
       transform: translate(-50%, -50%);
       background: brown;
   }
</style>

方法二:使用flex布局,通过align-items:centerjustify-content:center 设置容器的垂直和水平方向为居中对齐,然后它的子元素也可以实现垂直和水平的居中。

<style>
   .child {
       display: flex;
       height: 200px;
       justify-content: center;
       align-items: center;
       background: aqua;
   }
</style>

118.说说对Flex(弹性布局)布局的理解及其使用场景?

答:flex布局是CSS3新增的一种布局方式,可以通过将一个元素的display设置为flex,从而使它成为一个flex容器,它的所有子元素都会成为它的项目。一个容器默认有两条轴:一个是水平的主轴,一个是与主轴垂直的交叉轴,可以使用flex-direction来指定主轴的方向。可以使用justify-content来指定元素在主轴上的排列方式,使用align-items来指定元素在交叉轴上的排列方式,还可以使用flex-wrap来规定当一行排列不下时换行方式。

119.为什么需要清除浮动?清除浮动的方式有哪些?

答:清除浮动的主要原因是解决浮动元素导致父容器塌陷和布局混乱的问题,当一个元素设置了浮动属性后,它会脱离正常的文档流,导致父容器无法正确计算其高度,从而导致父容器塌陷。

  • 给父级元素定义一个height属性。
 .parent {
     background: brown;
     height: 300px;
 }
.child {
    float: right;
    background: aqua;
    height: 200px;
    width: 100px;
}
  • 最后一个浮动元素之后添加一个空的div标签,并添加clear:both样式
<div style="clear:both;"></div>
  • 使用overflow属性清除浮动,将包含浮动元素的父容器设置为'overflow:auto'或'overflow:hidden',使其形成BFC(块级格式上下文),从而清除浮动。
<div style="overflow:auto;">
    <!-- 浮动元素 -->
</div>
  • 使用伪元素清除浮动:将包含浮动元素的父容器添加一个伪元素,并为伪元素设置样式'clear:both'
.clearfix::after {
    content: "";
    display: table;
    clear: both;
}

120.对BFC的理解,如何创建BFC?

答:

概念: 块格式化上下文(Block Formatting Context,BFC)是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品的摆放,并且不会影响其他环境中的物品,如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。

创建BFC的条件:

  • 根元素:body
  • 元素设置浮动:float除none以外的值
  • 元素设置绝对定位:position(absolute、fixed)
  • display值为:felx、inline-block、table-cell、table-caption等
  • overflow值为:hidden、auto、scroll

BFC的作用:

  • 解决margin的重叠问题(css中规定垂直方向上两个相邻元素都有margin的话,取两者中最大的一个margin作为间隙): 由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变成两个BFC,就解决了margin重叠的问题。
  • 解决高度塌陷的问题: 在对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变成0,解决这个问题,只需要把父元素变成一个BFC,常用的方法是给父元素设置overflow:hidden
  • 创建自适应两栏布局: 可以用来创建自适应两栏布局,左边的宽度固定,右边的宽度自适应。
.left{
     width: 100px;
     height: 200px;
     background: red;
     float: left;
 }
 .right{
     height: 300px;
     background: blue;
     overflow: hidden;
 }
 
<div class="left"></div>
<div class="right"></div>

121. 什么是margin重叠问题?如何解决?

答:在垂直方向上,两个块级元素的上外边距和下外边距可能会合并为一个外边距,通常大小会取其中外边距值大的那个,这种行为就是margin重叠。

解决办法:

  • 使用BFC(块级格式上下文):将元素包裹在一个具有触发BFC条件的父级容器中,可以创建一个新的BFC环境,从而防止margin重叠,比如给父容器添加浮动,设置overflow除了visible以外的值,将元素设置为绝对定位等。

122.position的属性有哪些,区别是什么?(position、relative、fixed的区别有哪些?)

答:

  • relative:元素的定位永远是相对元素自身位置的,和其他元素没有关系,也不会影响其他元素。

image.png

  • fixed:元素的定位是相对于window边界的,和其他元素没有关系。但是他具有破坏性,会导致其他元素的位置变化。

image.png

  • absolute: 元素的定位相对于前两者要复杂许多,如果为absolute设置了top、left,浏览器会根据什么区确定他的纵向和横向的偏移量呢?答案是浏览器会递归查找该元素的所有父元素,如果找到一个设置了position:relative/absolute/fixed的元素,就以该元素为基准定位,如果没有找到,就以浏览器边界定位。

image.png

123.display、float、position的关系?

答:他们之间类似于一个优先级的机制,position:absoluteposition:fixed优先级最高,有它存在的时候,浮动不起作用,display的值也需要调整,其次,元素的float特征的值不是none的时候,或者它是根元素的时候,也会调整display的值,最后,只有是非根元素,并且是非浮动元素,并且是非绝对定位元素,display特征值同设置值。

124. absolute与fixed的共同点和不同点是什么?

答:他们两都是CSS中常用的定位方式,且都脱离了正常的文档流,不占据原有元素的空间。不同点在于绝对定位(absolute)是相对于父元素定位的,位置随滚动改变,固定定位(fixed)相对于浏览器窗口定位,位置固定不变。

124.css元素的层叠顺序是什么?

1.层叠上下文的创建

  • 根层叠上下文 body中的HTML标签,默认就是处于HTML这个根层叠上下文中。

  • 定位元素创建层叠上下文 元素position属性为relative、absolute、fixed时,并且z-index值不是auto时。

  • 其他css样式

    • 1.z-index的值不是auto的flex项
    • 2.元素opacity值不是1
    • 3.元素transform值不是none
    • 。。。。等

2.层叠顺序是怎么样的?

image.png 背景 < 负的z-index < block块级盒子 < float浮动盒子 < inline或inline-block盒子 < z-index为0的盒子 < 正的z-index

例题:

<style>
    .box {
        height: 200px;
        width: 200px;
        border: red solid;
    }
    .box1 {
        background: aqua;
        position: absolute;
        z-index: 5;
    }
    .box2 {
        background: yellow;
        position: absolute;
        margin-top: 50px;
        margin-left: 50px;
        z-index: 2;
    }
    .box3 {
        background: pink;
        position: absolute;
        margin-top: 100px;
        margin-left: 100px;
        z-index: 1;
    }
    
<div class="box box1">
    <div class="box box2">2</div>
    <div class="box box3">3</div>
</div>

</style>

显示:

image.png

因为1号方框,满足创建层叠上下文,故会创建一个全新的层叠上下文,它是2和3号的父元素,故他属于层叠上下文的背景。此时不管他的z-index多大都没有用。始终在最下面。 2号和3号是并列的,对比他们z-index即可。

125. 设置小于12px的字体?

在谷歌下css设置字体大小为12px及以下时,显示都是一样大小,都是默认12px。

  • 使用css3的transform缩放属性-webkit-transform:scale(0.5); 注意-webkit-transform:scale(0.75);收缩的是整个元素的大小,这时候,如果是内联元素,必须要将内联元素转换成块元素,可以使用display:block/inline-block/...;

  • 使用图片:如果是内容固定不变情况下,使用将小于12px文字内容切出做图片,这样不影响兼容也不影响美观。

125.手写实现一个三角形?

原理:CSS绘制三角形主要用到的是border属性,也就是边框。

div {
    width: 0;
    height: 0;
    border: 100px solid;
    border-color: orange blue red green;
}

image.png 只需要用transparent隐藏其他边就可以实现三角形了。

div {
    width: 0;
    height: 0;
    border-bottom: 50px solid red;
    border-right: 50px solid transparent;
    border-left: 50px solid transparent;
}

126.手写实现一个扇形?

原理:用CSS实现扇形的思路和三角形的基本一致,就是多了一个圆角的样式,实现一个90度的扇形。

div{
    border: 100px solid transparent;
    width: 0;
    heigt: 0;
    border-radius: 100px;
    border-top-color: red;
}

image.png

127.实现一个宽度自适应地正方形?

原理:marginpadding的百分比值只是和父元素的宽度有关,而与高度无关。

.square {
  width: 20%;
  height: 0;
  padding-top: 20%;
  background: orange;
}

image.png

128.实现一条0.5px的线?

原理:对于一个具有1像素高度和100像素宽度的元素,应用 transform: scale(0.5, 0.5) 后,元素的新高度将为0.5像素,但依然是1像素的线条。这是因为浏览器会将0.5像素进行取整,最小渲染单位仍然是1像素。

同样的,此方法也可以用于近似实现0.5像素线的效果。但请记住,实际上并没有真正的0.5像素线。 原理:

<style>
    .square {
        width: 1000px;
        height: 1px;
        background: red;
        transform: scale(1,0.5);
    }
</style>

129.isNaN和Number.isNaN函数的区别?

答:

  • 函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回true,因此非数字值传入也会返回true,会影响NaN的判断。
  • 函数Number.isNaN会首先判断传入的参数是否是数字,如果是数字在判断是否为NaN,不会进行数据类型转换,这种方法对于NaN的判断更为准确。

130.几个特殊的转换规则?

答:

  • Undefined类型转换为数字类型时的值转换为NaN。
  • Null类型转换为数字类型时会转换为0
  • String类型转换为数字类型时,如果是非数字的字符串是转换为NaN,如果是空字符串为0
  • 转换为布尔值时,undefined、null、false、+0、-0和NaN、空字符串都是false
  • null == undefined 是true,除此之外,undefined和null与谁比较,结果都为false
  • NaN无论与谁比较,包括它自己,结果都是false

131.Object.is()与比较操作符"==="、"=="的区别?

答:

  • 使用"=="进行相等判断时,如果两边的类型不一致,则会进行强制类型转换后再进行比较。
  • 使用三等号"==="进行相等判断时,如果两边的类型不一致时,不会做强制类型转换,直接返回false。
  • 使用Object.is()来进行相等判断时,一般情况下与三等号相同,处理了一些特殊情况,比如+0和-0不再相等,NaN和NaN是相等。

132.什么是javascript中的包装类型?

答:在javascript中,基本类型是没有属性和方法的,但是为了方便操作基本类型的值,在调用基本类型的属性和方法时,js会在后台,将基本类型转换成对象,比如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

var a = 'abc'
Object(a) // String {"abc"}

133.object.assign和扩展运算法是深拷贝还是浅拷贝,两者的区别是什么?

答:两者都是浅拷贝。

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象,然后把所有的源对象合并到目标对象中,它会修改一个对象,因此会触发ES6 setter。
let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
  • 扩展操作符(...)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中,它不复制继承的属性或类的属性,但是他会复制ES6的symbols属性。
let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

134.const对象的属性可以修改吗?

答:可以修改,const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动,对于基本类型的数据(数值,字符串,布尔类型),其值就保存在变量指向的那个内存地址,因此等同于常量。 但是对于引用类型(主要是对象和数组)而言,变量指向的数据的内存地址,保存的只是一个指针,const只能保证指针的固定不变,并不是保证数据结构不变。

135.如果new一个箭头函数会怎么样?

答:箭头函数是ES6提出来的,他没有prototype,也没有自己的this指向,更不能使用argument参数,所以不能new一个箭头函数。

new操作符的实现步骤: 1.创建一个空对象 2.将对象的__proto__指向构建函数的prototype属性,也就是指向原型对象。 3.执行构造函数中的代码,构造函数中的this指向该对象。 4.返回新对象。

136.箭头函数和普通函数的区别?

答:箭头函数比普通函数更加简洁,同时箭头函数没有自己的this,箭头函数this在定义时就已经确认,且永远不会改变,call()、apply()、bind()等方法也不能改变箭头函数中this指向,箭头函数不能作为构造函数使用,且没有自己的arguments和prototype,箭头函数不能用作Generator函数,不能使用yeild关键字。

137.箭头函数的this指向哪里?

答:箭头函数不同于传统的javascript函数,箭头函数并没有属于自己的this,它所谓的this是捕获其所在上下文的this值,作为自己this值。这个this在定义时确定,并且不会改变。

138.javascript有哪些内置对象?

答:js中的内置对象主要指的是在程序执行前存在全局作用域里的由js定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般用到的全局变量值NaN、undefined、全局函数如parseInt()、ParseFloat()用来实例化对象的构造函数如Date、Object等,数学计算的Math。

139.对JSON的理解?

答:JSON是一种基于文本的轻量级数据交换格式,它可以被任何的编程语言读取并作为数据格式来传递。JSON格式相比于js更加严格,属性值不能为函数、NaN等,故需要通过JSON.stringfy()函数将js对象转换成JSON,通过JSON.parse()函数解析。

140.javascript脚本延迟加载的方式有哪几种?

  1. 将脚本放在HTML文档底部,这样在页面加载完之后再加载脚本,从而不会阻塞页面的加载。
  2. 使用defer属性,相当于告诉浏览器立即异步下载脚本,但延迟执行。
  3. 使用async属性,相当于告诉浏览器立即异步下载脚本,并立即执行脚本

image.png

141.JavaScript类数组对象的定义?

答:一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。 类数组转换为数组的方法:

  1. 通过call调用数组的slice方法来实现转换。
Array.prototype.slice.call(arrayLike);
  1. 通过call调用数组的splice方法来实现
Array.prototype.splice.call(arrayLike, 0); //表示删除0个
  1. 通过call调用数组的concat方法来实现
Array.prototype.concat.apply([], arrayLike);
  1. 通过 Array.from 方法来实现转换
Array.from(arrayLike);

142.什么是DOM和BOM?

答:

  • Dom(Document Object Model)是一个表示网页文档结构的对象,他将网页文档以一个树状结构表示,并提供一系列的方法和属性,可以让开发者对网页文档进行操作和访问。

  • BOM(Browser Object Model)指的是浏览器对象模型,他指的是浏览器当做了一个对象来对待,这个对象定义了与浏览器进行交互的接口。BOM的核心是window,而window对象具有双重角色,既是js访问浏览器窗口的一个接口,又是一个全局对象。 -BOM浏览器对象模型的内置对象:

    1)window对象: BOM的核心对象是window,它表示浏览器的一个实例,它也是ECMAScript规定的Globle对象。

    2)  location对象: url地址相关的,常见属性有hash, protocal, host,hostname, pathname, port, search, href

    3)  history对象: 存储最近访问过的网址列表(即历史访问记录),多用于操作浏览器的"前进"和"后退"

    4)  navigator对象: 通过这个对象可以获得浏览者的浏览器的种类、版本号等属性。

    5)  screen对象: 用于存储浏览者系统的显示信息,如屏幕的分辨率、颜色深度等。

    location,history,navigator,screen都在window对象下

143.encodeURI、encodeURIComponent 的区别

答:

  • encodeURI用于对整个URL进行编码,但是会保留特殊字符。
  • encodeURIComponent用于对URL的组件进行编码,对特殊字符进行编码。

144.对AJAX的理解,实现一个AJax请求?

Ajax指的是通过javascript的异步通信,从服务器获取XML文档从中提取数据,再更新当前网页的对应部分。 步骤:

  • 创建一个XMLHttpRequest对象。
  • 使用open方法创建一个HTTp请求
  • 在发起请求前,添加一些信息和监听函数
  • 最后调用send方法来向服务器发送请求。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

145.use strict是什么意思?使用它区别是什么?

答:use strict是一种ES5添加的严格模式,这种模式使得JS在更加严格的条件下运行,主要目的是:消除js语法的不合理、不严谨之处,消除代码运行的不安全之处。在严格模式下,禁止使用with语句、this禁止指向全局对象,对象不能有重名的属性。

146.如何判断一个对象是否属于某个类?

答:

  • 第一种方式:通过instanceof运算符判断构造函数的原型是否出现在对象的原型链上
class Person {}
const p = new Person();
console.log(p instanceof Person);  // true
  • 第二种方法:判断对象的constructor属性获取对象的构造函数,与类的构造函数进行比较
class Person {}
const p = new Person();
console.log(p.constructor === Person);  // true
  • 第三种方法:通过Object.prototype.tostring()方法打印对象的[[class]]属性来进行判断
class Person {
  // 重写 toString() 方法返回类名信息
  toString() {
    return "[object Person]";
  }
}

const p = new Person();
console.log(p.toString());  // [object Person]
console.log(p.toString() === "[object Person]");  // true

147.强类型语言和弱类型语言的区别?

答:

  • 强类型语言:强类型语言是一种强制类型定义的语言,要求变量的使用严格符合定义,所有变量都必须先定义后使用。java和c++都是。
  • 弱类型语言:JS语言就是弱类型语言,简单理解就是一种变量类型可以被忽略的语言。比如字符串'12'和整数3相加,得到字符串'123',在相加的时候会进行强制类型转换。 对比:强类型语言在速度上可能比弱类型语言差,但是强类型语言带来的严谨性可以避免很多错误。

148.for...in和for...of的区别?

  • for...of只可以遍历可迭代对象,比如数组、字符串、Set、Map、类数组等,不能遍历普通的对象。for...in可用于遍历普通对象,但是会遍历整个原型链,性能较差。

149.this是什么?

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

  • 函数调用模式,当一个函数不是一个对象属性时,直接作为函数来调用,this指向全局对象
  • 方法调用模式,函数作为对象的一个方法来调用,则this指向该对象。
  • 构造器模式,使用new调用,函数执行会创建一个新对象,this指向该对象。
  • apply、call、bind调用,this绑定传入的第一个参数。

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

150.call()和apply()的区别?

答:

  • apply()接收两个参数,第一个参数是指定函数体内this的指向,第二个参数是一个带下标的集合,可以是数组和类数组,集合中的元素被作为参数传入给调用的函数。
  • call()传入的参数数量是不固定的,第一个参数是函数图this的指向,第二个参数开始,每个参数依次传入给调用的函数。

补充:call\apply\bind 中的thisArg参数是null、undefined 不起效果的,依然会指向外面的window.

但是在严格模式下,然是输出null或者 undefined

151.Math.round()是什么?

  • 1、Math.ceil  向上取整  例子:Math.ceil(-5.9) -5  Math.ceil(5.1) 6 解析:-5比-5.9大,因为要向上取整,即往大的那个方向取整,所以Math.ceil(-5.9)等于-5;6比5.1大,所以Math.ceil(5.1)等于6
  • 2、Math.floor  向下取整 例子: Math.ceil(-5.9) -6  Math.ceil(5.1)  5 3、Math.round  四舍五入取整 ,五入也是往大的方向取整, 例如,3.5 将舍入为 4,而 -3.5 将舍入为 -3。
  • 3、Math.round  四舍五入取整 ,五入也是往大的方向取整, 例如,3.5 将舍入为 4,而 -3.5 将舍入为 -3。

152.常用的正则表达式

image.png

image.png

image.png

image.png

案例:

image.png

153.进程和线程的区别?

答:进程和线程的区别有以下几点:

  • 1.进程时资源分配的基本单位,线程是cpu执行运算和调度的基本单位
  • 2.一个操作系统可以拥有很多进程,一个进程可以有很多线程
  • 3.每个进程都拥有自己的内存和资源,一个线程中的线程会共享这些内存和资源。

154. 介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景?

答:

观察者模式和发布订阅模式都有观察者和发布者这两个对象

观察者模式没有中介,发布者和订阅者必须知道对方的存在

发布订阅模式有中介,发布者和订阅者不需要知道对方是谁,只要通过中介进行信息的传递和过滤就可以了。

155.ES6 代码转成 ES5 代码的实现思路是什么?

答: 这个问题我们可以参考Babel的实现方式,大致分为三步:

  1. 解析:Babel首先将ES6代码解析为AST。这个AST捕获了代码的结构和语义
  2. 转换:然后,Babel会遍历AST,并对每个ES6特性应用转换。例如,它会找到所有的箭头函数并将它们转换为传统的函数声明
  3. 代码生成:最后,Babel将转换后的AST转换会JS代码。

156.CommonJs、AMD、CMD、ES6的区别?

答: 参考链接:juejin.cn/post/684490…

  • 1.CommonJS主要是为NodeJs设计的,适用于服务器端,特点是同步加载模块,使用'require'方法加载模块,使用'module.exports'导出模块。

  • 2.AMD主要是RequireJs提出,支持异步加载模块,定义模块使用define方法,适用于浏览器端,因为它可以异步加载模块。

  • 3.CMD主要是SeaJs提出,支持异步加载模块,特点是就近依赖,延迟加载,一个模块的依赖是在代码真正执行时才被加载,而不是定义时,适用于浏览器端。

  • 4.ES6 Moudules使用import和export语句来导入和导出模块,ES6 Modules 是静态加载的,既可以用在浏览器端也可以用在服务器端。是现在主流的模块化方案。

157.defienProperty与proxy区别?

1.definePropety

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

比如实现一个数据的监控功能

function Archiver() {
    var value = null;
    // archive n. 档案
    var archive = [];

    Object.defineProperty(this, 'num', {
        get: function() {
            console.log('执行了 get 操作')
            return value;
        },
        set: function(value) {
            console.log('执行了 set 操作')
            value = value;
            archive.push({ val: value });
        }
    });

    this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.num; // 执行了 get 操作
arc.num = 11; // 执行了 set 操作
arc.num = 13; // 执行了 set 操作
console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

2.Proxy

使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。

var proxy = new Proxy({}, {
    get: function(obj, prop) {
        console.log('设置 get 操作')
        return obj[prop];
    },
    set: function(obj, prop, value) {
        console.log('设置 set 操作')
        obj[prop] = value;
    }
});

proxy.time = 35; // 设置 set 操作

console.log(proxy.time); // 设置 get 操作 // 35

158.深度优先遍历和广度优先遍历的区别?

深度优先遍历(Depth First Search,DFS)是一种用于遍历或搜索树或图的算法。它从起始节点开始,递归地访问节点的所有未访问过的邻居节点,直到所有节点都被访问为止。在深度优先遍历中,会先探索最深的节点,因此得名深度优先。

广度优先遍历(Breadth First Search,BFS)也是一种用于遍历或搜索树或图的算法。它从起始节点开始,依次访问起始节点的所有邻居节点,再访问邻居节点的邻居节点,直到所有节点都被访问为止。在广度优先遍历中,会先探索离起始节点最近的节点,因此得名广度优先。

158. 从浏览器地址栏输入 url 到请求返回发生了什么

参考链接:juejin.cn/post/684490…

  1. 输入 URL 后解析出协议、主机、端口、路径等信息,并构造一个 HTTP 请求。
  • 强缓存。
  • 协商缓存。
  1. DNS 域名解析。(字节面试被虐后,是时候搞懂 DNS 了

  2. TCP 连接。

    总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。

  3. http 请求。

  4. 服务器处理请求并返回 HTTP 报文。

  5. 浏览器渲染页面。

    1. 解析HTML,构建DOM树
    1. 解析CSS,生成CSS规则树
    1. 合并DOM树和CSS规则,生成render树
    1. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
    1. 绘制render树(paint),绘制页面像素信息 。 image.png
  1. 断开 TCP 连接。

专项提升系列

image.png

1.this指针指向专题

链接:juejin.cn/post/702394…

  • 默认绑定: 非严格模式下 this 指向全局对象,严格模式下 this 会绑定为 undefined
  • 隐式绑定: 满足 XXX.fn() 格式,fnthis 指向 XXX。如果存在链式调用, this 永远指向最后调用它的那个对象
  • 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
  • 显式绑定: 通过 call/apply/bind 修改 this 指向
  • new绑定: 通过 new 来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的 this
  • 箭头函数绑定: 箭头函数没有 this ,它的 this 是通过作用域链查到外层作用域的 this ,且指向函数定义时的 this 而非执行时

题目一:隐式绑定与隐式绑定丢失

var x = 1;
var obj = {
    x: 3,
    fun:function () {
        var x = 5;
        return this.x;
    }
};

var fun = obj.fun;
console.log(obj.fun(), fun());

解析: JavaScript 对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。fun = obj.fun 相当于将 obj.fun 指向得堆内存指针赋值给了 fun,此后 fun 执行与 obj 不会有任何关系,发生隐式绑定丢失。

  • obj.fun(): 隐式绑定,fun 里面的 this 指向 obj,打印 3

  • fun(): 隐式绑定丢失: fun 默认绑定,非严格模式下,this 指向 window,打印 1

题目二:隐式绑定丢失

var person = {
  age: 18,
  getAge: function() {
    return this.age;
  }
};
var getAge = person.getAge
console.log(getAge())  // undefined

题目三:隐式绑定丢失

var obj = {
    name:"zhangsan",
    sayName:function(){
        console.log(this.name);
    }
}

var wfunc = obj.sayName;
obj.sayName();
wfunc();
var name = "lisi";
obj.sayName();
wfunc();

答案:

zhangsan
undefined
zhangsan
lisi

题目四:new绑定

var a = 5;
function test() { 
    a = 0; 
    console.log(a); 
    console.log(this.a); 
    var a;
    console.log(a); 
}
new test();
输出:
0
undefined
0

解析: 使用new来构建函数,会执行如下四步操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

通过 new 来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的 this

  • console.log(a): 打印变量 a 的值,当前 testAO 中存在 a 变量,打印 0
  • console.log(this.a): new 绑定 this 指向新的实例对象,当前题目没有给实例对象添加 a 属性,打印 undefined
  • console.log(a): 同第一个,打印 0

题目五:箭头函数与显式绑定

function fun () {
    return () => {
        return () => {
            return () => {
                console.log(this.name)
            }
        }
    }
}
var f = fun.call({name: 'foo'})
var t1 = f.call({name: 'bar'})()()
var t2 = f().call({name: 'baz'})()
var t3 = f()().call({name: 'qux'})

输出:

foo
foo
foo

解析:

  1. 箭头函数没有 this ,它的 this 是通过作用域链查到外层作用域的 this ,且指向函数定义时的 this 而非执行时。
  2. 箭头函数,不能通过 call\apply\bind 来修改 this 指向,但可以通过修改外层作用域的 this 来达成间接修改。
  3. JavaScript 是静态作用域,即函数的作用域在函数定义的时候就决定了,而箭头函数的 this 是通过作用域链查到的,因此箭头函数定义后,它的作用域链就定死了。
  • f = fun.call({name: 'foo'}): 将 fun 函数的 this 指向 {name: 'foo'},并返回一个箭头函数,因此箭头函数的 this 也指向 {name: 'foo'}
  • t1 = f.call({name: 'bar'})()(): 对第一层箭头函数执行 call 操作,无效,当前 this 仍指向 {name: 'foo'},第二层、第三层都是箭头函数,第三层的 this 也指向 {name: 'foo'},打印 foo
  • 后续 t2 t3 分别对第二层、第三层箭头函数使用 call ,无效,最终都打印 foo

题目六:箭头函数

let obj1 = {
    a: 1,
    foo: () => {
        console.log(this.a)
    }
}
// log1
console.log(obj1.foo())
const obj2 = obj1.foo
// log2
console.log(obj2())

//undefined
//undefined

解析:

  1. obj1.foo 为箭头函数,obj1 为对象,无法提供外层作用域,因此 obj.foo 里面的 this 指向 window
  • obj1.foo(): 箭头函数,this 指向 window,打印 undefined
  • obj2 隐式绑定丢失: 打印 undefined

题目七:综合题

var name = 'global';
var obj = {
    name: 'local',
    foo: function(){
        this.name = 'foo';
        console.log(this.name);
    }.bind(window)
};
var bar = new obj.foo();
setTimeout(function() {
    console.log(window.name);
}, 0);
console.log(bar.name);
 
var bar3 = bar2 = bar;
bar2.name = 'foo2';
console.log(bar3.name);

这个题的整体出题质量还是挺高的,首先咱们来把涉及到的知识罗列一下:

  1. bind 是显式绑定,会修改 this 指向,但 bind() 函数不会立即执行函数,会返回一个新函数
  2. setTimeout 是异步任务,同步任务执行完毕后才会执行异步任务
  3. 绑定优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

解析

  • obj.foo 将它的 this 通过 bind 显式的绑定为 window,但 bind 不会立即执行
  • var bar = new obj.foo(): new 绑定优先级大于 bind ,因此 bind 失效了,此时 this 指向 new 实例,因此 obj.foo 内部的 console 打印 foo
  • barnew obj.foo() 的实例,console.log(bar.name) 打印 foo
  • setTimeout 异步任务,等到同步执行完毕再来调用它的回调
  • bar3 = bar2 = bar,将 bar2,bar3,bar 的地址都指向 bar 所指向的空间。
  • bar2.name = 'foo2',修改地址指向堆内存的值
  • console.log(bar3.name): 由于三个变量指向同一块地址,bar3 修改了 namebar3 也随之改变,打印 foo2
  • setTimeout 的回调执行,打印 global

2.变量提升问题专题

链接:juejin.cn/post/693337…

image.png

  • 什么是变量提升?

定义:变量提升是当栈内存作用域形成时,JS代码执行前,浏览器会将带有var,function关键字的变量提前进行声明(值默认undefined),这种预先处理的机制就叫做变量提升机制。带 var 的只声明还没有被定义,带 function 的已经声明和定义。

  • 带var和不带var的区别

注意: var a=b=12相当于var a=12; b=12(b是没有var的) 分两种情况: 全局作用域中,带不带var都可以,自动属于window对象一个属性。 私有作用域(函数作用域),带有var的私有变量,不带的var的会向上级作用域查找,一直找到window为止,这个查找过程叫作用域链。 例子:

// 1
console.log(a, b)
var a =12, b ='林一一'
function foo(){
// 2
    console.log(a, b)
// 3
    var a = b =13
    console.log(a, b)
}
foo()
console.log(a, b)

/* 输出:
    undefined undefined
    undefined "林一一"
    13 13
    12 13
*/

思路:1处的 a, b 其实就是 window下面的属性为 undefined。在函数内部由于变量提升机制 avar 一开始就是 undefined,b不带var 将向上级作用域查找,找到全局作用域下的林一一所以2处打印出来的就是 undefined "林一一"。随后 a =13,window.b =13,即原来 b='林一一' 变成了 b=13,打印出13, 13,最后第4处打印出12, 13

例二:

a = 2
function foo(){
    var a =12;
    b = '林一一'
    console.log('b' in window)
    console.log(a, b)
}

foo()
console.log(b)
console.log(a)

/* 输出
    true
    12 "林一一"
    林一一
    2
/

思路:这是比较简单的一道题,需要注意的是函数内的 b 没有带 var,b 会一直向上查找到 window 下,发现 window 下也没有就直接给 window 设置了一个属性 window.b = '林一一',同理全局下的 a 也一样。

  • 等号左边下的变量提升

普通函数下变量提升例子:

print()
function print(){
    console.log('林一一')
}
print()

匿名函数下的带=的变量提升

print()
var print = function() {
    console.log('林一一')
}
print()
/*输出
    Uncaught TypeError: print is not a function
/

思路:同样由于变量提升机制带var的print是一开始值是undefined,所以print()这时还不是一个函数,所以报出类型错误。

  • 条件判断下的变量提升

if else判断下的变量提升,不管条件是否成立,都会进行变量提升

console.log(a)
if(false){
    var a = '林一一'
}
console.log(a)
/* 输出
    undefined
    undefined
/

if中()内的表达式不会变量提升

var y = 1
if(function f(){}){ 
    console.log(typeof f)  // undefined
    y = y + typeof f
}
console.log(y)  // 1undefined

理解:判断的条件没有提升,所以条件内部的f是未定义的。

  • 重名问题下的变量提升

带var和带function重名条件下的变量提升优先级,函数先执行。(js中函数是一等公民)

console.log(a);   
var a=1;
function a(){
    console.log(1);
}

// 或

console.log(a);   
function a(){
    console.log(1);
}
var a=1;
// 输出都是: ƒ a(){ console.log(1);}

理解:在 var 和 function 同名的变量提升的条件下,函数会先执行。所以输出的结果都是一样的。换一句话说,var 和 function 的变量同名 var 会先进行变量提升,但是在变量提升阶段,函数声明的变量会覆盖 var 的变量提升,所以直接结果总是函数先执行优先。

**函数名和 var 声明的变量重名 **

var fn = 12
function fn() {
    console.log('林一一')
}
console.log(window.fn)
fn()
/* 输出
*  12
*  Uncaught TypeError: fn is not a function
/

理解:带var声明的和带function声明的其实都是在window下的属性,也就是重名了,根据变量提升的机制,fn属于函数,函数会先执行,随着js代码自上而下执行时,此时fn是fn=12,输出window.fn=12,所以fn()==>12(),又是一个类型错误TypeError

变量重名在变量提升阶段会重新定义也就是重新赋值

console.log('1',fn())
function fn(){
    console.log(1)
}

console.log('2',fn())
function fn(){
    console.log(2)
}

console.log('3',fn())
var fn = '林一一'

console.log('4',fn())
function fn(){
    console.log(3)
}

/* 输出
*   3
*   1 undefined   //为什么是undefined,这里打印的是函数返回值,但是函数没有返回值
*   3
*   2 undefined
*   3
*   3 undefined
*   Uncaught TypeError: fn is not a function
/

思路:同样由于变量提升机制,fn 会被多次重新赋值最后赋值的地址值(假设为oxfffee)为最后一个函数,所以调用 fn都只是在调用最后一个函数输出都是 3, 代码执行到var fn = '林一一',所以 fn() 其实 == 林一一() 导致类型错误 TypeError

  • 函数形参的变量提升

函数的形参也会进行一次变量提升

function a(b){
  console.log(b);  
}
a(45);

// 等价于
// function a(b) {
//     var b = undefined;
//     b = 45;
// }
function foo(a) {
    console.log(a)
    var a
    console.log(a)
}
foo(a);
// 输出 1 1
  • 非匿名自执行函数的变量提升

匿名执行函数和非匿名自执行函数在全局环境下不具备变量提升的机制。

var a = 10;
(function c(){
})()
console.log(c)
// Uncaught ReferenceError: c is not defined

匿名自执行函数在自己的作用域内存在正常的变量提升

var a = 10;
(function(){
    console.log(a)
    a = 20
    console.log(a)
})()
console.log(a)
// 10, 20, 20

非匿名自执行函数的函数名在自己的作用域内变量提升,且修改函数名的值无效,这是非匿名函数和普通函数的差别

var a = 10;
(function a(){
    console.log(a)
    a = 20
    console.log(a)
})()
// ƒ a(){a = 20 console.log(a)}  ƒ a(){a = 20 console.log(a)}

理解:首先在全局环境下,var 声明的变量 a 会变量提升,但是非匿名函数不会在全局环境下变量提升因为具备自己的作用域了,而且上面的函数名 a 同样变量提升了,值就是函数 a 的应用地址值,输出的结果就是a(){a = 20 console.log(a)}而且非匿名自执行函数名是不可以修改的,即使修改了也不会有任何作用,严格模式下还会报错,所以最后输出的 a 还是 a(){a = 20 console.log(a)}

3.原型以及原型链专题

原型概念: JS在每个函数创建的时候,都会生成一个属性prototype,这个属性指向一个对象,这个对象就是此函数的原型对象。该原型对象有个constructor,指向该函数。

image.png

原型链概念: 当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数的原型对象,如果还没有找到,就会再其构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

image.png

image.png

4.javascript中如何进行隐式类型转换专题

ToPrimitive:如果值已经是原始类型,则返回它本身。否则,如果值有 valueOf() 方法,如果返回值为原始类型则返回 valueOf() 的结果。否则,如果值有 toString() 方法,如果返回值为原始类型则返回 toString() 的结果。否则,抛出 TypeError。

ToNumber:如果值已经是数字,则返回它本身。否则,如果值是一个对象,则尝试调用 valueOf() 方法,并将其结果转换为数字。否则,如果值是字符串,则尝试将其解析为数字,并返回解析的结果。否则,返回 NaN。

  • 情况一:+操作符,两边至少有一个string类型变量时,两边的变量会被隐式转换为字符串,其他情况两边变量都会被转换为数字。
1 + '23' // '123'
 1 + false // 1 
 1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
 '1' + false // '1false'
 false + true // 1
  • 情况二: -*、`` 操作符均转换成数字进行计算
1 * '23' // 23
 1 * false // 0
 1 / 'aa' // NaN
  • 情况三:对于==操作符
3 == true // false, 3 转为number为3true转为number为1
'0' == false //true, '0'转为number为0false转为number为0
'0' == 0 // '0'转为number为0
  • 4.情况四:对于<>比较符 如果两边都是字符串,则比较字母表顺序
'ca' < 'bd' // false
'a' < 'b' // true

其他情况都转换为数字再进行比较:

'12' < 13 // true
false > -1 // true

5.情况五:以上都是基本类型,如果涉及对象的话,则用ToPrimitive转换为基本类型再进行转换。

var a = {}
a > 2 // false

解析:
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果

5.javascript执行上下文专题

执行上下文概念: JS执行上下文是javascript代码运行时的环境,他决定了变量的作用域,函数的调用和对象的访问。分为全局执行上下文和函数执行上下文。

全局执行上下文的概念: 全局执行上下文只有一个,在客户端中一般由浏览器的js引擎创建,所有不在函数内部的js代码,都会在全局执行上下文中执行。

函数执行上下文的概念: 函数执行上下文理论上可能存在无数个,每当一个函数被调用时都会创建一个函数执行上下文,同一个函数被多次调用,都会创建一个新的上下文。

变量对象(VO)的概念: 变量对象(Variable Object)是javascript中的一个概念,它是在执行上下文(Execution Context)创建阶段被创建的一个特殊对象,用于存储变量和函数的定义。它包含以下内容:

  • 函数参数:如果当前执行上下文是一个函数的执行上下文,则函数的参数被作为变量对象的属性存储。
  • 函数声明:所有在当前执行上下文中定义的函数,无论是函数声明还是函数表达式,都被作为变量对象的属性存储,这允许在函数声明之前可以访问到这些函数。
  • 变量声明: 对于使用var声明的变量,在执行上下文创建阶段会被初始化为undefined并添加到变量中。

活动对象(AO)的概念: 活动对象(activation object)是指函数进入执行阶段时,原本不能访问的变量对象被激活成为了一个活动对象,我们可以访问其中的各种属性,其实变量对象和活动对象时一个东西,只是处于不同的状态和阶段。

作用域链的概念: 作用域规定了如何查找对象,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直查找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表叫做作用域链

当前可执行代码块的调用者(this value): 如果当前函数被作为对象方法调用或使用bind、call、apply等api进行委托调用,则将当前代码快的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

执行上下文数据结构模拟:

executionContext:{
    [variable object | activation object]:{
        arguments,
        variables: [...],
        funcions: [...]
    },
    scope chain: variable object + all parents scopes
    thisValue: context object
}

执行上下文的生命周期:

执行上下文的生命周期,分为三个阶段,分别是: 创建阶段、执行阶段、销毁阶段

  • 创建阶段(发生在函数调用时且执行函数体内的具体代码之前)

    1. 用当前函数的参数列表(arguments)初始化一个变量对象,并将当前执行上下文与之关联,函数代码中声明的变量和函数将作为属性添加到这个变量对象上。在这一阶段,会进行变量和函数的初始化声明,变量统一定义为undefined,需要等到赋值时才会有确切值,而函数则会直接定义。
    2. 构建作用域链
    3. 确定this的值
  • 执行阶段

    1. 执行阶段,JS代码开始逐条执行,在这个阶段,JS引擎开始对定义的变量赋值、开始顺着作用域链访问变量,如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出。
  • 销毁阶段

    1. 一般来讲,当函数执行完后,当前执行上下文会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。

    注意:闭包的情况下,由于闭包的作用域链仍然在引用父函数的变量对象,导致了父函数的变量对象会一直驻存于内存中,无法销毁,除非闭包的引用被销毁,闭包不再引用父函数的变量对象,这块内存才能释放掉。过度使用闭包会造成内存泄漏。

ES3执行上下文总结: 对于 ES3 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:

  1. 函数被调用

  2. 在执行具体的函数代码之前,创建了执行上下文

  3. 进入执行上下文的创建阶段:

    1. 初始化作用域链

    2. 创建 arguments object 检查上下文中的参数,初始化名称和值并创建引用副本

    3. 扫描上下文找到所有函数声明:

      1. 对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
      2. 如果函数名称已经存在了,属性的引用指针将会被覆盖
    4. 扫描上下文找到所有 var 的变量声明:

      1. 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined 来初始化
      2. 如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
    5. 确定 this

  4. 进入执行上下文的执行阶段:

    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

执行上下文栈的概念:

执行上下文栈是用于管理javascript执行上下文的数据结构,当JS引擎开始解析脚本代码时,会首先创建一个全局执行上下文,压入栈底(这个全局执行上下文从创建一直到程序销毁,都会存在于栈的底部),每当引擎发现一处函数调用,就会创建一个新的函数执行上下文压入栈内,并将控制权交给该上下文,待函数执行完成后,即将该执行上下文从栈内弹出销毁,将控制权重新给栈内上一个执行上下文。

例题:

第一题

var foo = function () {
    console.log('foo1');
}

foo();

var foo = function () {
    console.log('foo2');
}

foo();
// foo1
// foo2

理解:函数表达式的形式的定义,应该作为变量来对待。而不是作为函数。

第二题:

foo();

var foo = function foo() {
    console.log('foo1');
}

function foo() {
    console.log('foo2');
}

foo();

// foo2
// foo1

理解:由于函数声明优先级更高,所以函数声明在前,且如果var定义变量时发现已有同名的函数定义,则跳过变量定义。

第三题:

var foo = 1;
function bar () {
    console.log(foo);
    var foo = 10;
    console.log(foo);
}
bar();

// undefined
// 10

理解:foo带有var,所以本地作用域重新定义foo,故变量提升,输出undefined和10

第四题:

var foo = 1;
function bar () {
    console.log(foo);
    foo = 2;
}
bar();
console.log(foo);

//1
//2

理解:由于foo不带var,故直接从上层作用域找foo,所以第一个输出1, foo=2直接修改了上层作用域的值,故最后输出2

第五题:

var foo = 1;
function bar (foo) {
    console.log(foo);
    foo = 234;
}
bar(123);
console.log(foo);
//123
//1

理解:存在参数,参数可以转换为var foo = undefined; foo = 123;故先输出123,foo=234修改的是本地作用域。所以外层输出的1

第六题:

var a = 1;

function foo () {
    var a = 2;
    return function () {
        console.log(a);
    }
}

var bar = foo();
bar();

// 2

理解:函数能够访问到的上层作用域,是在函数声明时候就已经确定了的,函数声明在哪里,上层作用域就在哪里,和拿到哪里执行没有关系。

第七题:

"use strict";
var a = 1;

function foo () {
    var a = 2;
    return function () {
        console.log(this.a);
    }
}

var bar = foo().bind(this);
bar();

理解:这题考察的是执行环境中的 this 指向的问题,由于闭包内明确指定访问 this 中的 a 属性,并且闭包被 bind 绑定在全局环境下运行,所以打印出的是全局对象中的 a

总结:

  • 当函数运行的时候,会生成一个叫做 “执行上下文” 的东西,也可以叫做执行环境,它用于保存函数运行时需要的一些信息。

  • 所有的执行上下文都会被交给系统的 “执行上下文栈” 来管理,它是一个栈结构数据,全局上下文永远在该栈的最底部,每当一个函数执行生成了新的上下文,该上下文对象就会被压入栈,但是上下文栈有容量限制,如果超出容量就会栈溢出。

  • 执行上下文内部存储了包括:变量对象作用域链this 指向 这些函数运行时的必须数据。

  • 变量对象构建的过程中会触发变量和函数的声明提升。

  • 函数内部代码执行时,会先访问本地的变量对象去尝试获取变量,找不到的话就会攀爬作用域链层层寻找,找到目标变量则返回,找不到则 undefined

  • 一个函数能够访问到的上层作用域,在函数创建的时候就已经被确定且保存在函数的 [[scope]] 属性里,和函数拿到哪里去执行没有关系。

  • 一个函数调用时的 this 指向,取决于它的调用者,通常有以下几种方式可以改变函数的 this 值:对象调用、callbindapply

6.关于闭包的专题

闭包概念:能够访问其他函数内部的变量的函数,称为闭包。

闭包的应用场景

  • 单例模式

单例模式是一种常见的涉及模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:

// 单例模式
function Singleton(){
  this.data = 'singleton';
}

Singleton.getInstance = (function () {
  var instance;
    
  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      return instance;
    }
  }
})();

var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
  • 模拟私有属性 javascript 没有 java 中那种 public private 的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:
// 模拟私有属性
function getGeneratorFunc () {
  var _name = 'John';
  var _age = 22;
    
  return function () {
    return {
      getName: function () {return _name;},
      getAge: function() {return _age;}
    };
  };
}

var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined
  • 柯里化

柯里化概念:柯里化(currying)是一种将带有多个参数的函数转换成一系列嵌套、只接受单一参数的函数的技术。

function add(x, y, z) {
  return x + y + z;
}

// 使用柯里化转换函数
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...restArgs) {
        return curried.apply(this, args.concat(restArgs));
      };
    }
  };
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 输出:6
console.log(curriedAdd(1, 2)(3)); // 输出:6
console.log(curriedAdd(1)(2, 3)); // 输出:6

闭包的问题

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包

理解:javascript内部的垃圾回收机制用的是引用技术收集,即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为0的变量标记为失效变量并回收。 但是上述代码中,foo函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo运行完成后,内部的变量就应该被销毁,内存被回收。然后闭包导致了全局作用域始终存在一个baz的变量在引用这foo内部的bar函数,这就意味着foo内部定义bar函数引用数始终为1,垃圾运行机制无法把它销毁。导致内存泄漏。

内存泄漏

概念:内存泄漏是指一块内存不在被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。全局变量的无意创建、闭包过度使用、Dom的事件绑定未移除等都会导致。

内存泄漏的解决方案

1.使用严格模式,避免不经意间的全局变量泄漏

"use strict";

function foo () {
	b = 2;
}

foo(); // ReferenceError: b is not defined

2.关注 DOM 生命周期,在销毁阶段记得解绑相关事件:

const wrapDOM = document.getElementById('wrap');
wrapDOM.onclick = function (e) {console.log(e);};

// some codes ...

// remove wrapDOM
wrapDOM.onclick = null;
wrapDOM.parentNode.removeChild(wrapDOM);

3.避免过度使用闭包。

7.事件循环专题

1.事件循环的概念?

事件循环概念: 浏览器中的事件循环是一种机制,用于控制和调度javascript代码在浏览器中的执行的顺序,事件循环确保所有javascript代码的执行都是异步和非阻塞的,以确保用户界面的响应性。事件循环将任务分为两类,宏任务和微任务。

  • 宏任务包括script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI Render
  • 微任务包括process.nextTick、Promise(then\catch\finally)、Async/Await(await会把他后面的代码注册到微任务中,不包括他自己那行)、MutationObserver(html5新特性)

image.png

总结:执行一个宏任务,然后执行目前微任务队列中全部微任务,再执行一个宏任务,以此反复执行。

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')
 // 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析:

setTimeout(() => {
    task()
},3000)

sleep(10000000)
  • task()进入Event Table并注册,计时开始。

  • 执行sleep函数,很慢,非常慢,计时仍在继续。

  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。

  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

2.任务队列是什么?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

JavaScript语言的设计者意识到,主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

  • 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。浏览器本身存在一个监听的进程,当主线程执行完毕,他就会去通知异步任务队列,拿一个异步任务到主线程执行。

3.异步队列(eventQueue)是什么?

异步任务可分为宏任务、微任务,当遇到异步任务时,异步任务会进入到Event Table并注册函数,当指定的事情完成后,Event Table会将你数移入到Event Queue(异步队列)。当主线程的任务执行完毕之后,会去Event Queue中读取对应的函数到主线程执行。

  • macro-task(宏任务)

每次执行栈执行的代码就是一个宏任务,从事件队列中获取一个事件回调放入到执行栈中执行也是一个宏任务。浏览器为了能够使JS内部macro-task(宏任务)与DOM任务能够有序的执行,会在一个宏任务执行完成之后,在下一个宏任务开始之前,对页面进行重新渲染。

-   script(代码块)
-   setTimeout / setInterval
-   setImmediate
-   I/O
-   UI render
-   postMessage
-   MessageChannel
  • micro-task(微任务)

微任务是在运行宏任务/同步任务的时候产生的,是属于当前任务的,所以它不需要浏览器的支持,内置在 JS 当中,直接在 JS 的引擎中就被执行掉了。

可以理解是在宏任务执行完成之后立即执行的任务。他在渲染之前,无需等待渲染,所以的他响应速度要比宏任务快。在一个宏任务运行期间产生的所有微任务都在当前宏任务之前完成之后立即执行。

-   process.nextTick
-   Promise(then、catch、finally)
-   Async/Await
-   MutationObserver

4.渲染任务是什么?

其实在同步任务、异步任务之外还有渲染任务。页面并不是时时刻刻去渲染的,而是有他固定的节奏去渲染(render steps),一般情况浏览器的渲染是每秒60次,遵循W3C规则的浏览器是跟随电脑的刷新频率进行渲染。在它内部分为三个小步骤:

  • Structure - 构建 DOM 树的结构

  • Layout - 确认每个 DOM 的大致位置(排版)

  • Paint - 绘制每个 DOM 具体的内容(绘制)

5.特殊的requestAnimationFrame?

requestAnimationFrame是一个特殊的异步任务,他不会被加入到异步任务队列,而是被加入到渲染任务,他在渲染任务的三个步骤之前执行,用来处理渲染相关的工作。

6.requestAnimationFrame和setTimeout有什么不同

他们的不同可以从他们所属的任务找出不一样。requestAnimationFrame属于渲染任务setTimeout属于宏任务。同个一个例子再来看看他们的区别

scss
复制代码
functuin callBack() {
	move(); // 让元素移动1PX
    requestAnimationFrame(callBack);
}
scss
复制代码
functuin callBack() {
	move(); // 让元素移动1PX
    setTimeout(() => {
    	callBack();
    }, 0);
}

这两种方法来让 box 移动起来。但实际测试发现,使用 setTimeout 移动的 box 要比 requestAnimationFrame 速度快得多。这表明单位时间内 callback 被调用的次数是不一样的。

这是因为 setTimeout 在每次运行结束时都把自己添加到异步队列。等渲染过程的时候(不是每次执行异步队列都会进到渲染循环)异步队列已经运行过很多次了,所以渲染部分会一下会更新很多像素,而不是 1 像素。 requestAnimationFrame 只在渲染过程之前运行,因此严格遵守“执行一次渲染一次”,所以一次只移动 1 像素,是我们预期的方式。

如果在低端环境兼容,常规也会写作 setTimeout(callback,1000/60) 来大致模拟 60 fps 的情况,但本质上 setTimeout 并不适合用来处理渲染相关的工作。因此和渲染动画相关的,多用 requestAnimationFrame,不会有掉帧的问题(即某一帧没有渲染,下一帧把两次的结果一起渲染了)

8.异步编程专题

1.异步编程的实现方式?

答:

  • 回调函数:将函数作为参数传递给其他函数,当操作完成时调用回调函数,会造成回调地狱。
function doSomethingAsync(callback) {
   setTimeout(function() {
      callback("操作完成");
   }, 1000);
}

doSomethingAsync(function(result) {
   console.log(result);
});
  • Promise方式:使用Promise的方式可以将嵌套的回调函数作为链式调用。
function doSomethingAsync() {
   return new Promise(function(resolve, reject) {
      setTimeout(function() {
         resolve("操作完成");
      }, 1000);
   });
}

doSomethingAsync().then(function(result) {
   console.log(result);
});
  • generator的方式:生成器是一个特殊的函数,可以暂停其执行,并在需要时重新开始执行。
function* doSomethingAsync() {
   yield new Promise(function(resolve, reject) {
      setTimeout(function() {
         resolve("操作完成");
      }, 1000);
   });
}

const generator = doSomethingAsync();
generator.next().value.then(function(result) {
   console.log(result);
});
  • async 函数 的方式:async函数是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部运行到一个await语句的时候,如果语句返回一个promise对象,那么函数将会等待promise对象的状态变成resolve后再继续向下执行。

2.setTimeout、Promise、Async/Await的区别?

答:setTimeout是一个异步api,用于在指定时间后执行回调函数,Promise是ES6引入的一种处理异步操作的对象,Promise对象有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝),可以使用then()方法处理异步操作的结果,并用catch()方法处理错误。Async/Await是ES8引入的一种处理异步操作的语法,他是基于Promise对象的语法糖,通过async关键字定义一个异步函数,使用await关键字等待一个Promise对象解决,并以同步的形式处理异步操作。

3.介绍一下Promise是什么?

答: Promise对象是异步编程的一种解决方案,Promise是一个构造函数,接收一个函数作为参数,返回一个Promise实例。一个Promise实例有三种状态,分别是pending、resolved和rejected,分别代表了进行中,已成功和已失败。实例的状态只能由pending转变resolved和rejected状态,并且状态一经改变,就无法改变。同时状态的改变是通过resolve()和reject()函数来实现的,可以在异步操作结束后调用这两个函数改变Promise实例的状态,它的原型上定义了一个then方法,使用这个then方法可以为两个状态的改变注册回调函数,这个回调函数属于微任务。

4.Promise常见的方法有哪些?

  1. then()方法可以接受两个回调函数作为参数,第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象状态变为rejected时调用,其中第二个参数可以省略。
  2. catch()当Promise对象的状态变为rejected时调用。
  3. all()方法可以完成并行任务,它接收一个数组,数组的每一项都是一个promise对象,当数组中所有的promise的状态都达成resolved的时候,all方法的状态会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
  4. race()接受的参数是一个每项都是promise的数组,当最先执行完的事件执行完之后,就会直接返回该promise对象的值,如果第一个完成promise对象状态变成resolved,那自身的状态变成resolved。反之,第一个promise变成rejected,那么自身状态就变成rejected。 5.finally()方法用于不管Promise对象最后状态如何,都会执行的操作。

5.Promise解决了什么问题?

答:解决了以下几个问题:

  • 回调地狱:通过Promise的链式调用,可以避免多层嵌套的回调函数,使代码结构更加清晰简洁。
  • 异步操作结果的处理:Promise提供了then()方法,用于处理异步操作的结果,并将结果传递给下一个then()方法,使得处理异步操作的结果更加灵活和方便。
  • 异步操作错误的处理:promise提供了catch()方法,用于捕获和处理异步操作产生的错误。

6.对async/await的理解?

答:async函数是一个特殊的函数,它会返回一个promise对象,通过在函数前面加上async关键词,函数内部的代码就可以使用await关键字来等待一个异步操作的完成。在使用await关键字时,函数会暂停执行,直到等待的异步操作完成并返回结果,这样可以使得异步操作的写法看起来像同步操作,而不需要使用回调函数或者链式调用。

7.await到底在等待什么?

答:await表达式的运算结果取决于它等的是什么,如果它等到的不是一个promise对象,那await表达式的运算结果就是它等到的东西,如果它等到的是一个Promise对象,await就会阻塞后面的代码,等着Promise对象状态变为resolved,它返回的结果作为await表达式的值。

8.async/await对比Promise的优势是什么?

答:

  • 代码看起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调用也会带来额外的阅读负担。
  • Promise传递中间值比较复杂,而async和await几乎就是同步的写法,比较简单。
  • Promise需要使用then、catch链式的捕获错误,但是async/await可以使用try..catch,很容易。

9.Async/Await 如何通过同步的方式实现异步?

答:async awiat 是一种语法糖,基于Generator 函数和自动执行器实现。

function getData(){
    return new Promise(resolve=>{
        setTimeout(() => {
            console.log('done');
            resolve();
        }, 1000);
    })
}

function print(){
    console.log('print');
}

//async await 函数
function downloading(){
    function * loadingData(){ //Generator 函数
        var x1 = yield getData();
        var x2 = yield print();
        return 1;
    }
    function start(fn){ //自动执行器实现
        return new Promise((resolve,reject)=>{
            var it = fn();
            function run(value){
                var result = it.next(value);
                if(result.done){
                    resolve(result.value);
                    return;
                }
                Promise.resolve(result.value).then(data=>{
                    run(data);
                })
            }
            run();
        })
        
    }
    return start(loadingData);
}
downloading().then(v=>{console.log(v)})

垃圾回收机制专题

1.介绍一下js的垃圾回收机制GC(什么是垃圾回收机制)?

答:GC即Garbage collection,程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而GC就是负责回收垃圾的,这一套引擎执行则称为垃圾回收机制。

2. 为什么要进行垃圾回收?

答:程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。

3.垃圾回收策略有哪些?

主要有两种垃圾回收策略,第一种是标记清除算法,第二种是引用计数算法

标记清除算法执行过程如下: 垃圾收集器在运行时会给内存中所有变量加上一个标记,假设内存中所有对象都是垃圾,全标记为0; 然后从各个根对象开始遍历,把不是垃圾的结点改为1; 清理所有标记为0的垃圾,销毁并回收他们所占用的内存空间; 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收。

优点: 实现比较简单,打标记只有打和不打两种情况,这使得一位二进制位(0/1)就可以为其标记,非常简单。

缺点: 标记清除算法在清楚之后,剩余的内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片。

引用计数算法执行过程如下: 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值得引用次数为1 如果同一个值又被赋给另一个变量,那么引用数加1 如果该变量的值被其他的值覆盖了,则 引用次数减1 当这个值得引用次数为0时,说明没有变量在使用,这个值没法被访问了,垃圾回收器会在运行的时候清理掉有引用次数为0的值占用的内存。

优点: 引用计数在引用值为0时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾。

缺点: 无法解决循环引用无法回收的问题

function test(){
  let A = new Object()
  let B = new Object()
  
  A.b = B
  B.a = A
}

分代式垃圾回收 执行过程: 分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接手检查,新老生代的回收机制及频率是不同的,可以说此机制的出现提高了垃圾回收机制的效率。

4.如何减少垃圾回收?

答:

  • 对数组进行优化:在清空一个数组时,最简单的方式是将数组length设置为0,以此达到清空数组的目的。
  • 对object进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。

5.哪些情况会导致内存泄漏?

答:以下四种情况会造成内存泄漏:

  • 意外的全局变量: 由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直留在内存中无法回收。
  • 被遗忘的计数器或回调函数: 设置了setInteval定时器,忘记取消他,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离DOM的引用: 获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直都被留内存当中。

补充:

代码回收规则如下:

1.全局变量不会被回收。

2.局部变量会被回收,也就是函数一旦运行完以后,函数内部的东西都会被销毁。

3.只要被另外一个作用域所引用就不会被回收