JavaScript现在是一种非常流行的编程语言,基于该语言,派生了大量库和框架。 但是,无论高层生态系统如何发展,离不开原始的JavaScript。 在这里,我选择了4个JavaScript面试问题来测试程序员使用普通JavaScript的技能。
创新互联专注于企业网络营销推广、网站重做改版、丰都网站定制设计、自适应品牌网站建设、成都h5网站建设、商城网站建设、集团公司官网建设、外贸网站制作、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为丰都等各大城市提供网站开发制作服务。
1.实现Array.prototype.map
如何手动实现Array.prototype.map方法?
熟练使用数组的内置方法并不难。但是,如果您只是熟悉语法而又不了解原理,那么很难真正理解JavaScript。
对于Array.prototype.map,它将创建一个新数组,其中将填充在调用数组中每个元素上调用提供的函数的结果。
如果引用lodash,我们可以编写一个map函数,如下所示:
- function map(array, iteratee) {
- let index = -1
- const length = array == null ? 0 : array.length
- const result = new Array(length)
- while (++index < length) {
- result[index] = iteratee(array[index], index, array)
- }
- return result
- }
使用示例:
2. Object.defineProperty和代理
如何实现这种编码效果?
我们可以看到,当我们尝试连续打印obj.a三次时,会得到三种不同的结果。看起来多么不可思议!
您可以创建一个神秘的对象obj来实现此效果吗?
实际上,此问题有三种解决方案:
根据ECMAScript,对象的属性可以采用两种形式:
从逻辑上讲,对象是属性的集合。每个属性都是数据属性或访问器属性:
所谓的数据属性通常是我们写的:
let obj = { a: 1, b: 2}
我们对一个对象的属性只有两个操作:读取属性和设置属性。对于访问器属性,我们使用get和set方法定义属性,其编写方式如下:
- let obj = {
- get a(){
- console.log('triggle get a() method')
- console.log('you can do anything as you want')
- return 1
- },
- set a(value){
- console.log('triggle set a() method')
- console.log('you can do anything as you want')
- console.log(`you are trying to assign ${value} to obj.a`)
- }
- }
访问属性为我们提供了强大的元编程能力,因此我们可以通过以下方式满足我们的要求:
- let obj = {
- _initValue: 0,
- get a() {
- this._initValue++;
- return this._initValue
- }
- }
- console.log(obj.a, obj.a, obj.a)
第二种方法是使用Object.defineProperty,该方法的工作方式与我们用来访问属性的方法相同,除了不是直接声明访问属性,而是通过Object.defineProperty配置访问属性。
这使用起来更加灵活,因此我们可以这样编写:
- let obj = {}Object.defineProperty(obj, 'a', { get: (function(){ let initValue = 0; return function(){ initValue++; return initValue } })()})console.log(obj.a, obj.a, obj.a)
在这里的get方法中,我们使用了一个闭包,以便我们需要使用的变量initValue隐藏在闭包中,并且不会污染其他范围。
第三种方法是使用代理。
使用代理,我们可以拦截对对象属性的访问。 只要我们使用代理来拦截对obj.a的访问,然后依次返回1、2和3,我们就可以在以下条件之前完成要求:
- let initValue = 0;
- let obj = new Proxy({}, {
- get: function(item, property, itemProxy){
- if(property === 'a'){
- initValue++;
- return initValue
- }
- return item[property]
- }
- })
- console.log(obj.a, obj.a, obj.a)
为什么理解这个问题很重要?因为Object.defineProperty和Proxy给了我们强大的元编程能力,所以我们可以适当地修改对象以做一些特殊的事情。
在著名的前端框架Vue中,其核心机制之一是数据的双向绑定。在Vue2.0中,Vue通过使用Object.defineProperty实现了该机制。在Vue3.0中,使用Proxy完成此机制。
如果不掌握Vue之类的框架,您将无法真正理解。如果您掌握了这些原则,则只需学习Vue的一半,就可以获得两倍的结果。
3.范围和闭包
运行此代码的结果是什么?
- function foo(a,b) {
- console.log(b)
- return {
- foo:function(c){
- return foo(c,a);
- }
- };
- }
- let res = foo(0);
- res.foo(1);
- res.foo(2);
- res.foo(3);
上面的代码同时具有多个嵌套函数和三个foo嵌套函数,乍一看看起来非常繁琐。那么,我们如何理解这一点呢?
首先,请确保上面的代码中有多少个功能?我们可以看到在上面的代码中的两个地方都使用了关键字函数,因此上面的代码中有两个函数,即第一行函数foo(a,b) 和第四行 foo:function(c)。并且这两个函数具有相同的名称。
第二个问题:第5行的foo(c,a)调用哪个函数?如果不确定,让我们来看一个简单的示例:
- var obj={
- fn:function (){
- console.log(fn);
- }
- };
- obj.fn()
如果我们运行该代码,是否会引发异常? 答案是肯定的。
这是因为obj.fn()方法的上限是全局的,并且无法访问obj内部的fn方法。
回到前面的示例,以同样的逻辑,当我们调用foo(c,a)时,实际上是在第一行上调用foo函数。
当我们调用res.foo(1)时,将调用哪个foo? 显然,第4行的foo函数被调用。
因为这两个foo函数的工作方式不同,所以我们可以将其中一个的名称更改为bar,以使我们更容易理解代码。
- function foo(a,b) {
- console.log(b)
- return {
- bar:function(c){
- return foo(c,a);
- }
- };
- }
- let res = foo(0);
- res.bar(1);
- res.bar(2);
- res.bar(3);
此更改不会影响最终结果,但会使我们更容易理解代码。如果将来遇到类似的问题,请尝试此技巧。
每次调用一个函数时,都会创建一个新的作用域,因此我们可以绘制图表以帮助我们理解代码工作原理的逻辑。
当我们执行let res = foo(0);时,实际上是在执行foo(0,undefiend)。此时,将在程序中创建一个新的作用域,在当前作用域中a = 0,b = undefined。因此,我绘制的图看起来像这样。
然后将执行console.log(b),因此它第一次在控制台中打印出" undefined"。
然后执行res.bar(1),创建一个新范围,其中c = 1:
然后从上面的函数中再次调用foo(c,a),它实际上是foo(1,0),作用域如下所示:
在新作用域中,a的值为1,b的值为0,因此控制台将打印出0。
再次执行res.bar(2)。注意,res.bar(2)和res.bar(1)是并行关系,因此我们应该像这样绘制范围图:
因此,在此代码中,控制台也会打印出值0。
执行res.bar(3)的过程也是如此,控制台仍显示0。
因此,以上代码的最终结果是:
实际上,上述问题可以用其他方式改变。例如,可以将其更改为以下内容:
- function foo(a,b) {
- console.log(b)
- return {
- foo:function(c){
- return foo(c,a);
- }
- };
- }
- foo(0).foo(1).foo(2).foo(3);
在解决这个问题之前,我们要做的第一件事是区分两个不同的foo函数,因此可以将上面的代码更改为如下所示:
- function foo(a,b) {
- console.log(b)
- return {
- bar:function(c){
- return foo(c,a);
- }
- };
- }
- foo(0).bar(1).bar(2).bar(3);
执行foo(0)时,作用域与以前相同,然后控制台将打印出" undefined"。
然后执行.bar(1)创建一个新的作用域。此参数1实际上是c的值。
然后.bar(1)方法再次调用foo(c,a),它实际上是foo(1,0)。这里的参数1实际上将是新作用域中a的值,而0将是新作用域中b的值。
因此,控制台随后输出了b的值,即0。
再次调用.bar(2),在新作用域中c的值为2:
然后.bar(2)调用foo(c,a),它实际上是foo(2,1),其中2是新作用域中a的值,而1是新作用域中b的值。
因此,控制台随后输出了b的值,即0。
然后它将执行.bar(3),该过程与之前相同,因此我将不扩展其描述,此步骤控制台将打印出2。
如上所述,代码运行的最终结果是:
好了,经过漫长的旅程,我们终于得到了答案。 这个问题很好地检验了受访者对封闭和范围的理解。
4.撰写 Compose
假设我们有一个看起来像这样的函数:
- function compose (middleware) { // some code}
compose函数接受函数数组中间件:
- let middleware = []
- middleware.push((next) => {
- console.log(1)
- next()
- console.log(1.1)
- })
- middleware.push((next) => {
- console.log(2)
- next()
- console.log(2.1)
- })
- middleware.push(() => {
- console.log(3)
- })
- let fn = compose(middleware)
- fn()
当我们尝试执行fn时,它将调用中间件中的函数,并将下一个函数作为参数传递给每个小函数。
如果我们在一个小函数中执行next,则将调用中间件中该函数的next函数。而且,如果您接下来不执行,程序也不会崩溃。
执行完上面的代码后,我们得到以下结果:
1232.11.1
那么,我们如何编写一个compose函数来做到这一点呢?
首先,compose函数必须返回一个composed函数,因此我们可以编写如下代码:
- function compose (middleware) {
- return function () { }
- }
然后,在返回的函数中,中间件的第一个函数开始执行。我们还将传递下一个函数作为其参数。所以让我们这样写:
function compose (middleware) {
- function compose (middleware) {
- return function () {
- let f1 = middleware[0]
- f1(function next(){ })
- }
- }
下一个功能充当继续在中间件中运行的开关,如下所示:
- function compose (middleware) {
- return function () {
- let f1 = middleware[0]
- f1(function next(){
- let f2 = middleware[1]
- f2(function next(){ ... })
- })
- }
- }
然后继续在下一个函数中调用第三个函数…等待,这看起来像递归! 因此,我们可以编写一个递归函数来完成此嵌套调用:
- function compose (middleware) {
- return function () {
- dispatch(0)
- function dispatch (i) {
- const fn = middleware[i]
- if (!fn) return null
- fn(function next () {
- dispatch(i + 1)
- })
- }
- }
- }
好的,这就是我们的撰写功能,所以让我们对其进行测试:
好吧,此功能完全可以完成其所需的工作。 但是我们也可以优化我们的compose函数可以支持异步函数。 我们可以改进以下代码:
- function compose (middleware) {
- return async function () {
- await dispatch(0)
- function async dispatch (i) {
- const fn = middleware[i]
- if (!fn)
- return null
- await fn(function next () {
- dispatch(i + 1)
- })
- }
- }
- }
实际上,以上的撰写功能是众所周知的节点框架koa的核心机制。
当我选择候选人时,我接受他/她对某些框架不熟悉。毕竟,JavaScript生态系统中有太多的库和框架,没有人能完全掌握它们。但是我确实希望候选人知道这些重要的原始JavaScript技巧,因为它们是所有库和框架的基础。
结论
实际上,我的草稿中还有其他一些面试问题,但由于本文篇幅有限,因此在此不再继续解释。稍后再与您分享。
本文主要涉及普通JavaScript,而不涉及浏览器,节点,框架,算法,设计模式等。如果您对这些主题也感兴趣,请随时发表评论。
标题名称:这4个问题可以检测出你JavaScript水平的高低
文章URL:http://www.36103.cn/qtweb/news21/8121.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联