2023前端二面常考面试题合集5

2023-01-09 09:13:54 浏览数 (1)

说下对 JS 的了解吧

是基于原型的动态语言,主要独特特性有 this、原型和原型链。

JS 严格意义上来说分为:语言标准部分(ECMAScript) 宿主环境部分

语言标准部分

2015 年发布 ES6,引入诸多新特性使得能够编写大型项目变成可能,标准自 2015 之后以年号代号,每年一更

宿主环境部分

  • 在浏览器宿主环境包括 DOM BOM 等
  • 在 Node,宿主环境包括一些文件、数据库、网络、与操作系统的交互等

说一下SPA单页面有什么优缺点?

代码语言:javascript复制
优点:

1.体验好,不刷新,减少 请求  数据ajax异步获取 页面流程;

2.前后端分离

3.减轻服务端压力

4.共用一套后端程序代码,适配多端

缺点:

1.首屏加载过慢;

2.SEO 不利于搜索引擎抓取

为什么有时候⽤translate来改变位置⽽不是定位?

translate 是 transform 属性的⼀个值。改变transform或opacity不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。⽽改变绝对定位会触发重新布局,进⽽触发重绘和复合。transform使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此translate()更⾼效,可以缩短平滑动画的绘制时间。 ⽽translate改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。

代码输出结果

代码语言:javascript复制
var a = 1;
function printA(){
  console.log(this.a);
}
var obj={
  a:2,
  foo:printA,
  bar:function(){
    printA();
  }
}

obj.foo(); // 2
obj.bar(); // 1
var foo = obj.foo;
foo(); // 1

输出结果: 2 1 1

解析:

  1. obj.foo(),foo 的this指向obj对象,所以a会输出2;
  2. obj.bar(),printA在bar方法中执行,所以此时printA的this指向的是window,所以会输出1;
  3. foo(),foo是在全局对象中执行的,所以其this指向的是window,所以会输出1;

代码输出结果

代码语言:javascript复制
const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

输出结果如下:

代码语言:javascript复制
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

这里其实是一个坑,.then.catch 返回的值不能是 promise 本身,否则会造成死循环。

代码输出结果

代码语言:javascript复制
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

输出结果如下:

代码语言:javascript复制
1

看到这个题目,好多的then,实际上只需要记住一个原则:.then.catch 的参数期望是函数,传入非函数则会发生值透传

第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个then里,直接打印出1。

参考 前端进阶面试题详细解答

代码输出结果

代码语言:javascript复制
var friendName = 'World';
(function() {
  if (typeof friendName === 'undefined') {
    var friendName = 'Jack';
    console.log('Goodbye '   friendName);
  } else {
    console.log('Hello '   friendName);
  }
})();

输出结果:Goodbye Jack

我们知道,在 JavaScript中, Function 和 var 都会被提升(变量提升),所以上面的代码就相当于:

代码语言:javascript复制
var name = 'World!';
(function () {
    var name;
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye '   name);
    } else {
        console.log('Hello '   name);
    }
})();

这样,答案就一目了然了。

代码输出结果

代码语言:javascript复制
 var a = 10; 
 var obt = { 
   a: 20, 
   fn: function(){ 
     var a = 30; 
     console.log(this.a)
   } 
 }
 obt.fn();  // 20
 obt.fn.call(); // 10
 (obt.fn)(); // 20

输出结果: 20 10 20

解析:

  1. obt.fn(),fn是由obt调用的,所以其this指向obt对象,会打印出20;
  2. obt.fn.call(),这里call的参数啥都没写,就表示null,我们知道如果call的参数为undefined或null,那么this就会指向全局对象this,所以会打印出 10;
  3. (obt.fn)(), 这里给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于 obt.fn(),所以会打印出 20;

说一下常见的git操作

代码语言:javascript复制
git branch 查看本地所有分支
git status 查看当前状态 
git commit 提交 
git branch -a 查看所有的分支
git branch -r 查看远程所有分支
git commit -am "nit" 提交并且加注释 
git remote add origin git@192.168.1.119:ndshow
git push origin master 将文件给推到服务器上 
git remote show origin 显示远程库origin里的资源 
git push origin master:develop
git push origin master:hb-dev 将本地库与服务器上的库进行关联 
git checkout --track origin/dev 切换到远程dev分支
git branch -D master develop 删除本地库develop
git checkout -b dev 建立一个新的本地分支dev
git merge origin/dev 将分支dev与当前分支进行合并
git checkout dev 切换到本地dev分支
git remote show 查看远程库
git add .
git rm 文件名(包括路径) 从git中删除指定文件
git clone git://github.com/schacon/grit.git 从服务器上将代码给拉下来
git config --list 看所有用户
git ls-files 看已经被提交的
git rm [file name] 删除一个文件
git commit -a 提交当前repos的所有的改变
git add [file name] 添加一个文件到git index
git commit -v 当你用-v参数的时候可以看commit的差异
git commit -m "This is the message describing the commit" 添加commit信息
git commit -a -a是代表add,把所有的change加到git index里然后再commit
git commit -a -v 一般提交命令
git log 看你commit的日志
git diff 查看尚未暂存的更新
git rm a.a 移除文件(从暂存区和工作区中删除)
git rm --cached a.a 移除文件(只从暂存区中删除)
git commit -m "remove" 移除文件(从Git中删除)
git rm -f a.a 强行移除修改后文件(从暂存区和工作区中删除)
git diff --cached 或 $ git diff --staged 查看尚未提交的更新
git stash push 将文件给push到一个临时空间中
git stash pop 将文件从临时空间pop下来

vue-router

代码语言:text复制
vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度集成,让构建但页面应用变得易如反掌

<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址

<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。

<keep-alive> 组件是一个用来缓存组件

router.beforeEach

router.afterEach

to: Route: 即将要进入的目标 路由对象

from: Route: 当前导航正要离开的路由

next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

介绍了路由守卫及用法,在项目中路由守卫起到的作用等等

setInterval 模拟 setTimeout

描述:使用setInterval模拟实现setTimeout的功能。

思路setTimeout的特性是在指定的时间内只执行一次,我们只要在setInterval内部执行 callback 之后,把定时器关掉即可。

实现

代码语言:javascript复制
const mySetTimeout = (fn, time) => {
    let timer = null;
    timer = setInterval(() => {
        // 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
        clearInterval(timer);
        fn();
    }, time);
    // 返回用于关闭定时器的方法
    return () => clearInterval(timer);
}

// 测试
const cancel = mySetTimeout(() => {
    console.log(1);
}, 1000);  
// 一秒后打印 1

代码输出结果

代码语言:javascript复制
Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res)
  }, (err) => {
    console.log('error', err)
  }).catch(err => {
    console.log('catch', err)
  })

输出结果如下:

代码语言:javascript复制
error err!!!

我们知道,.then函数中的两个参数:

  • 第一个参数是用来处理Promise成功的函数
  • 第二个则是处理失败的函数

也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。

在这道题中,错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'

但是,如果是像下面这样:

代码语言:javascript复制
Promise.resolve()
  .then(function success (res) {
    throw new Error('error!!!')
  }, function fail1 (err) {
    console.log('fail1', err)
  }).catch(function fail2 (err) {
    console.log('fail2', err)
  })

then的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的catch捕获到。

Vue路由守卫有哪些,怎么设置,使用场景等

代码语言:text复制
常用的两个路由守卫:router.beforeEach 和 router.afterEach

每个守卫方法接收三个参数:

to: Route: 即将要进入的目标 路由对象

from: Route: 当前导航正要离开的路由

next: Function: 一定要调用该方法来 resolve 这个钩子。

在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。
判断是否登录,是否拿到对应的路由权限等等。

代码输出结果

代码语言:javascript复制
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

输出结果如下:

代码语言:javascript复制
1
7
6
8
2
4
3
5
9
11
10
12

(1)第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2

宏任务Event Queue

微任务Event Queue

setTimeout1

process1

setTimeout2

then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了process1then1两个微任务:

  • 执行process1,输出6。
  • 执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

(2)第二轮时间循环从**setTimeout1**宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2
  • new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

宏任务Event Queue

微任务Event Queue

setTimeout2

process2

then2

第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:

  • 输出3。
  • 输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。

  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3

宏任务Event Queue

微任务Event Queue

process3

then3

第三轮事件循环宏任务执行结束,执行两个微任务process3then3

  • 输出10。
  • 输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

代码输出结果

代码语言:javascript复制
console.log(1)

setTimeout(() => {
  console.log(2)
})

new Promise(resolve =>  {
  console.log(3)
  resolve(4)
}).then(d => console.log(d))

setTimeout(() => {
  console.log(5)
  new Promise(resolve =>  {
    resolve(6)
  }).then(d => console.log(d))
})

setTimeout(() => {
  console.log(7)
})

console.log(8)

输出结果如下:

代码语言:javascript复制
1
3
8
4
2
5
6
7

代码执行过程如下:

  1. 首先执行script代码,打印出1;
  2. 遇到第一个定时器,加入到宏任务队列;
  3. 遇到Promise,执行代码,打印出3,遇到resolve,将其加入到微任务队列;
  4. 遇到第二个定时器,加入到宏任务队列;
  5. 遇到第三个定时器,加入到宏任务队列;
  6. 继续执行script代码,打印出8,第一轮执行结束;
  7. 执行微任务队列,打印出第一个Promise的resolve结果:4;
  8. 开始执行宏任务队列,执行第一个定时器,打印出2;
  9. 此时没有微任务,继续执行宏任务中的第二个定时器,首先打印出5,遇到Promise,首选打印出6,遇到resolve,将其加入到微任务队列;
  10. 执行微任务队列,打印出6;
  11. 执行宏任务队列中的最后一个定时器,打印出7。

代码输出结果

代码语言:javascript复制
function foo(something){
    this.a = something
}

var obj1 = {
    foo: foo
}

var obj2 = {}

obj1.foo(2); 
console.log(obj1.a); // 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4)
console.log(obj1.a); // 2
console.log(bar.a); // 4

输出结果: 2 3 2 4

解析:

  1. 首先执行obj1.foo(2); 会在obj中添加a属性,其值为2。之后执行obj1.a,a是右obj1调用的,所以this指向obj,打印出2;
  2. 执行 obj1.foo.call(obj2, 3) 时,会将foo的this指向obj2,后面就和上面一样了,所以会打印出3;
  3. obj1.a会打印出2;
  4. 最后就是考察this绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出4。

代码输出结果

代码语言:javascript复制
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  
        console.log(self.foo);  
        (function() {
            console.log(this.foo);  
            console.log(self.foo);  
        }());
    }
};
myObject.func();

输出结果:bar bar undefined bar

解析:

  1. 首先func是由myObject调用的,this指向myObject。又因为var self = this;所以self指向myObject。
  2. 这个立即执行匿名函数表达式是由window调用的,this指向window 。立即执行匿名函数的作用域处于myObject.func的作用域中,在这个作用域找不到self变量,沿着作用域链向上查找self变量,找到了指向 myObject对象的self。

说一说什么是跨域,怎么解决

代码语言:html复制
因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
为来防止CSRF攻击
1.JSONP
    JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。    通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。    <script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
    <script>
        function jsonp(data) {            console.log(data)        }    </script>
    JSONP 使用简单且兼容性不错,但是只限于 get 请求。
2.CORS
    CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
3.document.domain
    该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。

    只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
4.webpack配置proxyTable设置开发环境跨域
5.nginx代理跨域
6.iframe跨域
7.postMessage
    这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

代码输出结果

代码语言:javascript复制
function a(xx){
  this.x = xx;
  return this
};
var x = a(5);
var y = a(6);

console.log(x.x)  // undefined
console.log(y.x)  // 6

输出结果: undefined 6

解析:

  1. 最关键的就是var x = a(5),函数a是在全局作用域调用,所以函数内部的this指向window对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的x变量的值是window,这里的x将函数内部的x的值覆盖了。然后执行console.log(x.x), 也就是console.log(window.x),而window对象中没有x属性,所以会输出undefined。
  2. 当指向y.x时,会给全局变量中的x赋值为6,所以会打印出6。

虚拟DOM转换成真实DOM

描述:将如下 JSON格式的虚拟DOM结构转换成真实DOM结构。

代码语言:javascript复制
// vnode 结构
{
    tag: 'DIV',
    attrs: {
        id: "app"
    },
    children: [
        {
            tag: 'SPAN',
            children: [
                {
                    tag: 'A',
                    children: []
                }
            ]
        }
    ]
}
// 真实DOM 结构
<div id="app">
    <span>
        <a></a>
    </span>
</div>

实现

代码语言:javascript复制
function _render(vnode) {
    // 如果是数字类型转化为字符串;
    if(typeof vnode === "number") {
        vnode = String(vnode);
    }
    // 字符串类型直接就是文本节点
    if(typeof vnode === "string") {
        return document.createTextNode(vnode);
    }
    // 普通 DOM
    const dom = document.createElement(vnode.tag);
    if(vnode.attrs) {
        // 遍历属性
        Object.keys(vnode.attrs).forEach((key) => {
            dom.setAttribute(key, vnode.attrs[key]);
        });
    }
    // 子数组进行递归操作
    vnode.children.forEach((child) => dom.appendChild(_render(child)));
    return dom;
}

// 测试
let vnode = {
    tag: "DIV",
    attrs: {
        id: "app",
    },
    children: [
        {
            tag: "SPAN",
            children: [
                {
                    tag: "A",
                    children: [],
                },
            ],
        },
    ],
};
console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>

0 人点赞