Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
885 lines (570 loc) · 38.5 KB

File metadata and controls

885 lines (570 loc) · 38.5 KB
Copy raw file
Download raw file
Outline
Edit and raw actions

JS 部分


以下题目整理自 front-end-interview-handbook


front-end-interview-handbook

解释一下事件代理

事件代理是指在一系列元素的祖先元素上绑定事件处理函数,而不是分别绑定在所有元素上。事件代理的原理是事件冒泡。

优点

  1. 降低内存使用,因为只需要无论有多少个元素,都只需要在祖先元素上绑定一个处理函数,一定程度上降低了内存的使用。
  2. 在新增/移除子元素的时候不需要给子元素绑定/解绑处理函数。

解释一下 this 的运行机制

this 的值取决于函数被调用的方式,它是在运行时确定的,属于动态作用域。

关于 this 的值,有以下几种可能(函数调用方式),当多种调用方式同时存在时,优先级从高到低:

  1. new
  2. apply、call、bind
  3. obj.method()
  4. 单独调用 func()(此时 this 是 window 或者 undefined)

解释一下原型链继承的运行机制

在 JS 中,每个对象都有一个 .__proto__ 属性,指向另一个对象,也就是它的原型。如果我们访问一个对象属性,但这个属性不存在的时候,JS 就会沿着 .__proto__ 去它的原型对象上找,原型对象也有自己的 .__proto__ 属性,所以这个查找过程会持续到找到相应属性或者到达原型链尽头。这种行为一般称为代理而不是继承。

AMD vs CommonJS

两者都是 ES6 之前的模块系统。

  1. CommonJS 是同步的,AMD(Asynchronous Module Definition) 是异步的。
  2. CommonJS 是设计用于服务端的,而 AMD 则倾向于浏览器,所以它支持异步加载模块。

在 ES6 提供原生模块系统支持后,两者都变得没有必要了。虽然 ES6 模块的支持还不全面,但这个问题可以用编译器来解决。

如何区分 null, undefined, undeclared?

  1. null: 已声明已赋值
  2. undefined: 已声明未赋值
  3. undeclared: 未声明

如何检测

null 和 undefined 可以使用全等直接和值比较,undeclared 可以使用 try/catch 在严格模式中捕获 Reference Error。

什么是闭包?

闭包是指一个函数加上它被声明时所处的词法作用域。闭包函数可以访问它外层函数的作用域,即使是在外层函数已经执行完毕之后。

有什么用

  1. 一般用于封装(模拟)私有属性和方法,常见于模块模式
  2. 用于实现 partial 或 curry

宿主对象(host objects)和原生对象(native objects)有什么不同?

原生对象是 JS 的一部分,在 ES 规范中有定义的,比如 String, Math, RegExp, Object, Function 等。

宿主对象是由运行环境提供的(浏览器/Node),比如 window, XMLHttpRequest 等。

https://stackoverflow.com/questions/7614317/what-is-the-difference-between-native-objects-and-host-objects

.call.apply 的区别?

.call.apply 都是用来调用函数并修改 this 指向的,不同的只是它们接收其他参数的方式,.call 接收若干个参数,用逗号分隔开,.apply 接收一个数组。

解释一下 .bind

.bind 会返回一个新函数并设定好 this 的指向。

一般是要把 class 的方法作为另一个函数的参数时(比如作为事件处理函数)会需要用到,避免函数在参数传递的过程中 this 发生意外的改变。

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind

解释一下 document.write()

当页面加载完毕之后再调用 document.write() 的话,它会调用 document.open() 方法,把整个页面都移除(<head><body> 都没了),然后替换成参数字符串。

document.open() 会打开一个文档流(document stream),document.write() 就往里面写入字符串。

在以前 document.write() 的确是有用途的,比如可以实现“当浏览器支持 JS 时才应用某些样式”的功能,或者实现“并行加载 JS 文件并保证执行顺序”的功能。

但是在现在,这些功能也不一定非要依靠 document.write() 来实现了。

https://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html

https://github.com/h5bp/html5-boilerplate/wiki/Script-Loading-Techniques#documentwrite-script-tag

特性检测(feature detection)、特性推理(feature inference)和 UA 字符串的区别是什么?

  • 特性检测是指检测浏览器是否支持某些功能,Modernizr 是一个用于特性检测的库。e.g. 'geolocation' in navigator
  • 特性推理和特性检测差不多,不过它是检查浏览器是否支持另一个方法,如果浏览器支持方法 A,特性推理就认为浏览器也支持方法 B。特性推理并非推荐的做法,还是用特性检测比较靠谱。
  • UA 字符串可以通过 navigator.userAgent 来获取。这是浏览器提供的一个属性,包含了程序类型、操作系统、软件厂商或者软件版本等信息。不过,这个信息并非完全可信,比如 Chrome 提供的 UA 字符串会说它自己是 Chrome 和 Safari。这个方法也应该避免。

https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Feature_detection

https://stackoverflow.com/questions/20104930/whats-the-difference-between-feature-detection-feature-inference-and-using-th

https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent

解释一下 Ajax

Ajax(asynchronous JavaScript and XML) 是实现异步程序的一系列技术,利用 Ajax,程序可以异步地向服务器发送请求、获取数据、更新页面,而不用刷新整个页面。现在一般都用 JSON 代替 XML 来进行数据传输了。常用到的 API 是 XMLHttpRequest 或者 fetch

https://en.wikipedia.org/wiki/Ajax_(programming)

https://developer.mozilla.org/en-US/docs/AJAX

阐述一下 Ajax 的优点和缺点

优点

  • 提高用户体验:可以局部更新网页内容,不用整个页面刷新。
  • 减少 js 和 css 文件的下载次数:如果页面整个刷新,文档中的资源都要重新下载;而使用 ajax 的话,就只需要下载一次。
  • 可以保持页面状态:因为页面没有刷新,所以状态也不会被重置。
  • 其他 SPA 的优点。

缺点

  • 动态网页对于添加书签并不友好。
  • 必须运行在支持 js 的浏览器上。
  • 对爬虫不友好,一些爬虫不会执行 js 脚本,也就爬不到内容。
  • 首屏时间长,SPA 页面要等到 js 加载执行完才能看到页面内容。
  • SPA 的其他缺点。

说一下 JSONP 的原理,为什么它不是 Ajax?

JSONP(JSON with Padding) 是用来绕过浏览器同源策略的一个方法,因为 Ajax 请求会受到同源策略的限制。

它利用 <script> 来向跨域域名发起请求,一般同时会指定一个 callback 作为参数,比如 https://example.com?callback=printDataprintData 需要在全局中定义。服务器收到请求后,会返回一个 js 文件,里面的内容类似:

printData({ name: 'suukii' });

浏览器接收到这个文件后执行里面的代码,这样就实现了从跨域域名请求数据的功能。

但由于服务器返回的是一个 js 文件,JSONP 其实存在着不小的安全漏洞,所以除非请求域名是可信任的,不然不要轻易使用 JSONP 技术。另外,在 CORS 出现后,我们也基本不需要 JSONP 了。

https://stackoverflow.com/a/2067584/1751946

解释一下“变量/函数提升”(hoisting)

变量提升是用来解释“变量在声明前就可以被访问”这个现象的,简单地说就是把 var 声明语句“提升”到全局/模块/函数代码的顶端,但被提升的仅仅是声明语句,赋值语句并没有被提升。函数声明则是整个函数体都会被提升。

letconst 声明的变量也都会被提升,但存在一个“暂时性死区”,在 letconst 声明语句执行之前,这些变量都不能被访问到。

“提升”其实并不是真实存在的行为,只是为了容易理解而提出来的一个概念。实际上在代码执行之前,JS 引擎还会有一个编译的阶段,在这个过程中它会解析声明语句,确定哪些作用域里面存在哪些变量。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_Types#Variable_hoisting

https://stackoverflow.com/questions/31219420/are-variables-declared-with-let-or-const-not-hoisted-in-es6/31222689#31222689

描述一下事件冒泡

当一个 DOM 元素上触发了某个事件时,它会先检查有没有事件处理函数,然后再把事件传递给它的父元素,如此重复,一直到 document 元素。

事件冒泡是事件代理的原理。

"attribute" 和 "property" 的区别是什么?

attribute 是定义在 HTML 文档中的,而 property 是定义在 DOM 元素上的。

https://stackoverflow.com/questions/6003819/properties-and-attributes-in-html

为什么不推荐扩展 JS 的内置对象?

因为如果直接在 prototype 上增加属性或者方法的话,很有可能会与第三方库或者将来的 JS 原生方法产生命名冲突。

除了提供 polyfill,最好不要直接拓展内置对象的 prototype

http://lucybain.com/blog/2014/js-extending-built-in-objects/

document 的 loadDOMContentLoaded 事件的区别是什么?

  • DOMContentLoaded 是在 HTML 文档下载解析完之后触发的,不用等待其他资源如样式、图片、subframe 完成加载。
  • load 事件则要等到 DOM 和所有其他资源都下载完成之后才会触发。

https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded

https://developer.mozilla.org/en-US/docs/Web/Events/load

===== 的区别是什么?

  • == 在比较之前会进行类型转换。
  • === 在比较之前不会进行类型转换,如果两个操作数类型不一致就直接返回 false。

什么时候可以使用 ==?一个小建议,在需要判断一个值是否等于 null 或者 undefined 的时候,为了方便,可以利用下 == 的类型转换。

https://stackoverflow.com/questions/359494/which-equals-operator-vs-should-be-used-in-javascript-comparisons

解释一下同源策略(从 JS 相关的角度)

同源策略限制了 JS 向跨域域名发送请求。同源指的是协议、hostname、端口名一致。

这样做是为了避免网页执行了恶意脚本,然后恶意脚本通过操作 DOM 来获取敏感信息。

https://en.wikipedia.org/wiki/Same-origin_policy

use strict; 有什么用?它的优缺点是什么?

用来开启全局或者函数的严格模式。

优点

  • 给未声明的变量赋值时会抛出错误而不是默默创建一个全局变量。
  • 尝试删除不允许删除的对象属性时会报错而不是静默失败。
  • 要求函数参数命名唯一。
  • 单独调用函数时 this 的值是 undefined 而不是 window
  • 纠正了一些其他 JS 的缺陷。

缺点

  • 不能访问 function.callerfunction.arguments 了。
  • 合并严格模式和非严格模式的代码可能会导致意想不到的问题。
  • 禁用了一些非严格模式中的特性。

不过总的来说,还是推荐使用严格模式。

http://2ality.com/2011/10/strict-mode-hatred.html

http://lucybain.com/blog/2014/js-use-strict/

解释一下什么是 SPA 以及如何提高 SPA 的 SEO

传统的网站是,浏览器从服务器接收 HTML 文档并渲染,如果用户跳转到了另一个链接,服务器会返回一份新的 HTML 文档,浏览器会重新渲染,这样每次都要更新整个页面,这个就叫做服务端渲染。

但 SPA 用的是客户端渲染,浏览器会先下载一个文档,下载文档中包含的脚本(框架、库、源码)和样式,然后再开始执行脚本、渲染页面。当需要导航到另一个链接时,页面的 URL 会通过 HTML5 的 History API 来更新,然后程序通过 Ajax 从服务器下载新数据,更新到页面上。这种模式更接近原生程序。

优点

  • 网站能更快地响应用户操作,不用一个操作一次全页面更新。
  • 减少了 HTTP 请求,包含在文档中的 JS 和 CSS 文件等资源只需要在第一次加载文档时下载,因为此后页面不再更新,这些资源也不需要重复下载了。
  • 更好实现了服务端和客服端的关注点分离。不同客户端上的网页应用可以对应同一套服务端代码,客户端和服务端通过约定好的 API 通信,各自技术栈不受对方约束。

缺点

  • 首次加载需要加载的资源比较多,比如框架代码、程序代码、公用资源。
  • 需要在服务端进行设置把客户端的所有路由都重定向到同一个入口,然后让客户端接手路由管理,避免客户端页面刷新。
  • SEO 不友好。SPA 要等 JS 加载完毕之后才会开始获取数据、渲染页面,但很多爬虫都不会执行 JS,所以就爬不到网页内容。如果需要考虑 SEO,可以考虑使用服务端渲染,或者使用 Prerender 之类的服务。

https://github.com/grab/front-end-guide#single-page-apps-spas

http://stackoverflow.com/questions/21862054/single-page-app-advantages-and-disadvantages

http://blog.isquaredsoftware.com/presentations/2016-10-revolution-of-web-dev/

https://medium.freecodecamp.com/heres-why-client-side-rendering-won-46a349fadb52

解释一下 Promise

Promise 是一个对象,保存着一个结果,这个结果在将来某一时刻才会被确定。

它有 3 种状态:

  1. 在结果确定之前,处于 pending 状态
  2. 成功的结果,resolved,返回数据
  3. 失败的结果,rejected,可能是报错了,返回错误信息

常见的 polyfill 有 $.deferred, Q 和 Blvebird,不过不是所有 polyfill 都完全遵循规范来实现的。

https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261

Promise 相对于回调的优缺点是什么?

优点

  1. 避免回调地狱。
  2. 编写顺序异步程序更简单。
  3. 有了 Promise.all(),写并行异步也更简单了。
  4. 关于回调的几个问题:过早调用、过晚调用、调用次数过多/过少、参数缺失、吞并异常等问题都不会发生。

缺点

  1. 兼容性,老浏览器可能不支持。

https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md

用另外一门语言写代码再编译成 JS 这样做的优缺点是什么?

优点

  1. 新语言解决了一些 JS 的历史遗留问题,还会限制使用 anti-patterns。
  2. 还可能提供一些语法糖,减少代码量。
  3. 对于大项目,静态检查(TS)是很有必要的。

缺点

  • 多了一个打包/编译的过程,因为浏览器只能执行 JS。
  • 如果 sourcemap 没有对应到编译前的代码的话,debug 会变得很麻烦。
  • 要考虑团队的学习成本。
  • 社区可能会比较小,资源/教程/工具不多。
  • IDE 支持可能不足。
  • 这些语言总会落后于最新的 JS 规范。

https://softwareengineering.stackexchange.com/questions/72569/what-are-the-pros-and-cons-of-coffeescript

使用什么工具和技术来 debug

https://hackernoon.com/twelve-fancy-chrome-devtools-tips-dc1e39d10d9d

https://raygun.com/blog/javascript-debugging/

如何遍历对象和数组?

遍历对象的键

  1. for...in
  2. Object.keys()
  3. Object.getOwnPropertyNames()

遍历数组

  1. for loop
  2. for...of,如果获取下标和值,可以用 arr.entries() 方法
  3. .forEach()

http://2ality.com/2015/08/getting-started-es6.html#from-for-to-foreach-to-for-of

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries

解释一下 mutable 和 immutable 对象

mutable 就是普通的 JS 对象。

immutable 指的是对象在创建之后,状态不能再修改。immutable 是函数式编程中一个很重要的原则。

如何创建 immutable 对象?

  • 禁止改写属性:设置 writable: falseconfigurable: false
  • 禁止添加属性:Object.preventExtensions(...)
  • Object.seal():相当于 Object.preventExtensions(...) + configurable: false,但还可以修改属性值。
  • Object.freeze():相当于 Object.seal() + writable: false

immutability 的优缺点

优点:

  • 容易追踪修改。而且对象的比较可以直接比较引用,在 React 和 Redux 中很有用。
  • 可预测,immutable 对象创建后状态就不会被修改了,不用担心某一个操作在将来会有什么影响。
  • 不用因为担心不小心修改到原对象而每次操作都自己复制一份对象。
  • 在多线程环境中也能放心使用,不用担心修改对象会相应其他线程。
  • 使用类似 ImmutableJS 的库可以提升性能,减少内存消耗。

缺点:

  • 原生实现的性能太差,所以需要借助库来实现。
  • 如果对象很多的话,频繁的内存分配还是会影响性能。
  • 存在循环引用的结构很难实现 immutable。(如果你有两个对象,它们初始化之后都不能再改变,那你要如何实现它们相互引用?)

https://github.com/yangshun/front-end-interview-handbook/blob/master/contents/en/javascript-questions.md

怎么实现 immutable?

  1. 用库:immutablejs, mori, immer
  2. 自己实现:用 const + 上面提到的冻结对象的方法。需要"修改"对象时,使用展开符、Object.assign()Array.concat() 等方法来创建新的对象。

https://stackoverflow.com/questions/1863515/pros-cons-of-immutability-vs-mutability

https://www.sitepoint.com/immutability-javascript/

https://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/

解释一下同步和异步函数的区别?

  • 同步函数会阻塞主线程,异步函数不会。
  • 同步函数中,按顺序执行,上一个语句执行完才会执行下一个语句,如果执行时间过长,程序就会变成无法响应了。
  • 异步函数则一般接收一个回调函数,等异步操作结束之后再调用回调。

什么是事件循环?调用栈和任务队列的区别又是什么?

事件循环是一个单线程的无限循环,它管着调用栈和任务队列,如果调用栈里面是空的,而任务队列中又有任务的话,事件循环就会从任务队列中出列一个任务,推入调用栈去执行。

https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html

https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html

http://theproactiveprogrammer.com/javascript/the-javascript-event-loop-a-stack-and-a-queue/

function foo() {}var foo = function () {} 有什么区别?

前一个是函数声明,后一个是函数表达式。关键区别在于函数声明中的函数体会被全部提升,所以可以在函数声明之前调用一个函数。而函数表达式中只有 var 声明语句被提升了,如果尝试在赋值钱调用函数,则会得到 TypeError。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function

let, var, const 的区别?

var let/const
作用域 函数/全局 块级,花括号(函数、if-else、for-loop)
提升 存在提升 存在提升,但存在暂时性死区
重复声明 覆盖 报错

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const

ES6 class 和 ES5 的构造函数有什么区别?

最主要的是在写继承的时候提供了更简洁的语法糖,但是继承原理还是一样的。

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance

https://eli.thegreenplace.net/2013/10/22/classical-inheritance-in-javascript-es5

说一下箭头函数的优点。

  • 语法简洁。
  • 提供词法作用域的 this,箭头函数的 this 在书写时就确定为它外层第一个普通函数的 this

在构造函数中使用箭头函数有什么好处?

将实例方法作为事件处理函数回调时,不用担心 this 丢失。

在 React 的 class 组件中常用,可以省掉手动 bind 的步骤。

https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb

高阶函数是什么?

接收函数作为参数,或者返回一个函数的函数。用来抽象一些操作,比如 map, forEach, bind

https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99

https://hackernoon.com/effective-functional-javascript-first-class-and-higher-order-functions-713fde8df50a

https://eloquentjavascript.net/05_higher_order.html

什么是柯里化?

柯里化指的是讲一个接收多个参数的函数,变成多个接收一个参数的函数,每次调用都只传入一个参数。

这个技术是函数式编程中常见的,用于提高代码可读性和可组合性(compose)。

function curry(fn) {
    if (fn.length === 0) {
        return fn;
    }

    function _curried(depth, args) {
        return function (newArgument) {
            if (depth - 1 === 0) {
                return fn(...args, newArgument);
            }
            return _curried(depth - 1, [...args, newArgument]);
        };
    }

    return _curried(fn.length, []);
}

function add(a, b) {
    return a + b;
}

var curriedAdd = curry(add);
var addFive = curriedAdd(5);

var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

https://hackernoon.com/currying-in-js-d9ddc64f162e

文件间如何共享代码?

取决于环境。

  • 浏览器:全局(window)、AMD(requirejs)
  • Node.js:CommonJS

不过 ES6 的模块系统最终应该会取代 AMD 和 CommonJS,统一客户端和服务器的模块系统。

http://requirejs.org/docs/whyamd.html

https://nodejs.org/docs/latest/api/modules.html

http://2ality.com/2014/09/es6-modules-final.html

为什么会需要 class 静态成员?

  • 保存配置
  • 静态方法一般是纯函数

https://stackoverflow.com/questions/21155438/when-to-use-static-variables-methods-and-when-to-use-instance-variables-methods


以下题目整理自神三元的博客


JS 基础

谈谈你对闭包的理解

概念

闭包是指那些能够访问其他函数作用域中变量的函数,也有定义是说这样的函数加上它能访问的作用域就构成了闭包。

本质/产生原因

之所以会产生闭包,首先是因为 JS 中存在作用域链这样一个机制,每个函数在创建时就会在其环境变量中保存对其父级作用域的引用,即使是创建这个函数的上下文(父级函数)已经执行完毕并销毁了,这个作用域引用还是有效的。

作用域链:当要访问一个变量时,JS 解释器首先会在当前作用域进行查找,如果找不到就会顺着作用域链一级一级往上查找,直到找到想要的变量或者到达作用域链的尽头。

表现形式

  1. 函数作为另一个函数的返回值
  2. 函数作为另一个函数的参数进行传递
  3. 定时器、Ajax、事件监听等异步操作中使用的回调函数
  4. IIFE

其实从闭包的本质可以看出,只要函数保存了对其父级作用域的引用,就会产生闭包,不拘于以上所提到的表现形式。

什么是原型和原型链?

原型

在 JS 中,每个函数都有一个 prototype 属性,指向一个对象,当我们把这个函数当作构造函数来调用时,生成的实例对象都会跟函数的 prototype 对象关联起来,而 prototype 对象就是实例对象的原型,实例对象可以通过委托(也有说法是继承)来访问它的原型对象上的属性和方法。

原型链

在 JS 中,对象都有自己的原型对象,而原型对象本身也是一个对象,所以它也有自己的原型对象,就这样子串成了原型链。当访问一个对象的属性时,如果属性不存在,解释器会去该对象的原型上查找,如果还是找不到,则会去原型对象的原型上查找,重复这个步骤直到找到了需要访问的属性,或者到达了原型链的尽头。

JS 中如何实现继承?

TODO

谈谈你对 BigInt 的理解

TODO

JS 深入数组

数组扁平化的几种方法

方法 1:递归 + reduce

const flatten = arr => {
    return arr.reduce((res, item) => {
        return Array.isArray(item)
            ? [...res, ...flatten(item)]
            : [...res, item];
    }, []);
};

方法 2:序列化 + replace

const flatten = arr => {
    return JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
};

方法 3:调 API flat()

const flatten = arr => {
    return arr.flat(Infinity);
};

方法 4:扩展运算符 + concat

const flatten = arr => {
    let res = [...arr];
    while (res.some(Array.isArray)) {
        res = [].concat(...res);
    }
    return res;
};

V8 的 sort() 用的是什么排序算法?

TODO

JS API 原理

如何模拟实现一个 new 的效果?

new 做的几个事情:

  1. 创建一个空对象
  2. 将构造函数中的 this 指向这个空对象
  3. 运行构造函数代码
  4. 如果函数返回值不是引用类型就返回第一步创建的对象

我们要做的事:

  • 首先基于构造函数的原型新建一个空对象,实现实例可以访问原型对象属性的效果。
  • 将新对象作为 this 调用构造函数。
  • 判断构造函数的返回值,如果不是引用类型的值,就返回第一步创建的对象,否则直接返回构造函数返回值。
const newFactory = (ctor, ...arg) => {
    if (typeof ctor != 'function') {
        throw TypeError('the first argument must be a function');
    }

    const thisObj = Object.create(ctor.prototype);
    const returnedValue = ctor.apply(thisObj, arg);

    return isObject(returnedValue) || isFunction(returnedValue)
        ? returnedValue
        : thisObj;

    // *******************************************
    function isObject(val) {
        return typeof returnedValue == 'object' && returnedValue;
    }

    function isFunction(val) {
        return typeof returnedValue == 'function';
    }
};

如何模拟实现一个 bind 的效果?

  • 对于普通函数调用,绑定 this 就行。
  • 对于构造函数调用,要将绑定后函数的原型对象指向原函数的原型。
  • 由于 new 的优先级高于 bind,所以要保证用 new 调用绑定函数时要忽略之前传入的 this 而使用 new 出来的对象。
Function.prototype.bind = function (thisArg, ...arg1) {
    if (typeof this != 'function')
        throw TypeError('cannot call bind on non-function');

    const func = this;

    const bound = function (...arg2) {
        const context = this instanceof func ? this : thisArg;
        return func.apply(context, [...arg1, ...arg2]);
    };

    bound.prototype = Object.create(func.prototype);
    return bound;
};

如何实现一个 call/apply 函数?

  • 先把函数挂在传入的 this 对象上作为方法
  • 调用该方法
  • this 对象上删掉该方法
  • 使用了 Symbol 避免命名冲突

call

Function.prototype.call = function (thisArg, ...arg) {
    if (typeof this != 'function')
        throw TypeError('cannot call call on non-function');

    const funcNameSymbol = Symbol('tempFunction');
    thisArg[funcNameSymbol] = this;

    const res = thisArg[funcNameSymbol](...arg);
    delete thisArg[funcNameSymbol];

    return res;
};

apply

Function.prototype.call = function (thisArg, arg) {
    // 跟 call 一样的
};

JS 中浅拷贝的手段有哪些?

数组浅拷贝

  1. 展开运算符,[...target]
  2. target.slice(0)
  3. [].concat(target)
  4. for...in + hasOwnProperty 自己实现

对象浅拷贝

  1. 展开运算符,{...target}
  2. Object.assign({}, target)
  3. for...in + hasOwnProperty 自己实现

如何写一个完整的深拷贝?

第一步:递归拷贝

const deepClone = target => {
    if (isArray(target) || isObject(target)) {
        const copy = isArray(target) ? [] : {};

        for (const prop in target) {
            if (target.hasOwnProperty(prop)) {
                const value = target[prop];
                copy[prop] =
                    isArray(value) || isObject(value)
                        ? deepClone(value)
                        : value;
            }
        }
        return copy;
    }
    return target;

    // *******************************************
    function isArray(target) {
        return Array.isArray(target);
    }

    function isObject(target) {
        return typeof target == 'object' && target;
    }
};

第二步:解决循环引用,用 WeakMap 记录已经处理过的值

const deepClone = (target, map = new WeakMap()) => {
    if (map.has(target)) return target;

    if (isArray(target) || isObject(target)) {
        map.set(target, true);
        // ...
    }
    // ...
};

用 WeakMap 的弱引用避免内存泄漏。

第三步:拷贝特殊对象

TODO

V8 引擎原理

JS 数据是怎么存储的?

V8 的内存分为 栈内存堆内存

  • 存储在 中的是不变的数据,包括 函数调用栈基础类型的值,还有 引用类型的引用地址
  • 存储在 中的是可变的数据,也就是引用类型的值,也是垃圾回收发生的地方。

拓展:为什么不把所有数据都存在栈内存?

因为系统栈除了存储变量,还有创建和切换执行上下文(栈帧)的功能。要实现快速切换上下文的功能,就需要每个栈帧的大小都是可预测的,这样才可以通过计算很快得出下个栈帧的内存地址,进行切换。如果在栈中放可变数据,可能需要将每个栈帧的大小设计得非常大,极大地增加了数据存储的空间复杂度,而且也提高了栈溢出的风险。

拓展:为什么不把所有数据都存在堆内存?

堆内存中数据杂乱,GC 算法复杂,会导致执行上下文切换开销极大。

V8 引擎如何进行垃圾内存的回收?

栈内存

对于栈内存来说,当 ESP 指针下移进行上下文切换之后,栈顶的空间就会被自动回收了。

堆内存

对于堆内存,情况复杂一点。

V8 的堆内存分成 新生代老生代 两个部分,刚新建出来的对象会先存在 新生代 内存中,如果对象存活时间比较久,就会被转移到 老生代 中。

新生代

新生代 又分成两个部分:FromToFrom 表示正在使用的内存,To 表示暂时闲置的内存。

  • 程序创建的对象会先被存在 From 内存中。
  • 当进行垃圾回收的时候,V8 会检查 From 中的对象,将存活对象复制到 To 内存,将非存活对象回收。
  • 然后两块内存的角色调换。

为什么新生代内存需要分成两部分?因为堆内存在使用上并不是连续的,所以会产生很多内存碎片,FromTo 这一步就是为了清理内存碎片。

新生代垃圾回收算法叫做 Scavenge 算法。

老生代

新生代 中的对象如果经过多次回收依然存在,就会被转移到 老生代 内存中,这种现象叫做 晋升,晋升发生的情况有以下两种:

  • 对象已经经历过一次 Scavenge 算法
  • To 内存的空间占用超过 25%

老生代中的内存回收采用的是 标记-清除,先遍历堆中所有对象,给它们做上标记,然后对于程序中 使用的对象 和 被 强引用的对象 取消标记,这个阶段称为标记阶段;接下来在清除阶段,对有标记的对象进行回收。

对于老生代中的内存碎片问题,V8 是直接在清除阶段把存活对象全部往一边靠,这个移动对象的过程也是最耗时间的。

由于垃圾回收是很耗时间的操作,而且会阻塞 JS 业务代码的执行。为了解决这个问题,V8 采用了 增量标记 的方案,也就是将上面说到的标记,清除,和处理内存碎片的阶段分成小段执行,而不是一口气执行到底。比如先用对一部分对象进行标记,然后暂停垃圾回收,执行业务代码,再回来继续标记对象。

描述一下 V8 执行一段代码的过程

TODO

如何理解 EventLoop?

Morty Proxy This is a proxified and sanitized view of the page, visit original site.