写点什么

京东前端一面面试题

作者:bb_xiaxia1998
  • 2022 年 9 月 11 日
    浙江
  • 本文字数:11725 字

    阅读完需:约 38 分钟

对 JSON 的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。


在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。


因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。


在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,


  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。

  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

JavaScript 中如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:


/*** @obj 需要转换的对象* @type 期望的结果类型*/ToPrimitive(obj,type)
复制代码


type的值为number或者string


(1)当typenumber时规则如下:


  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;

  • 调用objtoString方法,后续同上;

  • 抛出TypeError 异常。


(2)当typestring时规则如下:


  • 调用objtoString方法,如果为原始值,则返回,否则下一步;

  • 调用objvalueOf方法,后续同上;

  • 抛出TypeError 异常。


可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:


  • 如果对象为 Date 对象,则type默认为string

  • 其他情况下,type默认为number


总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:


var objToNumber = value => Number(value.valueOf().toString())objToNumber([]) === 0objToNumber({}) === NaN
复制代码


而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。


以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):


  1. +操作符 +操作符的两边有至少一个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. -*\操作符


NaN也是一个数字


1 * '23' // 23 1 * false // 0 1 / 'aa' // NaN
复制代码


  1. 对于==操作符


操作符两边的值都尽量转成number


3 == true // false, 3 转为number为3,true转为number为1'0' == false //true, '0'转为number为0,false转为number为0'0' == 0 // '0'转为number为0
复制代码


  1. 对于<>比较符


如果两边都是字符串,则比较字母表顺序:


'ca' < 'bd' // false'a' < 'b' // true
复制代码


其他情况下,转换为数字再比较:


'12' < 13 // truefalse > -1 // true
复制代码


以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:


var a = {}a > 2 // false
复制代码


其对比过程如下:


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


又比如:


var a = {name:'Jack'}var b = {age: 18}a + b // "[object Object][object Object]"
复制代码


运算过程如下:


a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步a.toString() // "[object Object]"b.valueOf() // 同理b.toString() // "[object Object]"a + b // "[object Object][object Object]"
复制代码

宏任务和微任务分别有哪些

  • 微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

  • 宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

let、const、var 的区别

(1)块级作用域: 块作用域由 { }包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:


  • 内层变量可能覆盖外层变量

  • 用来计数的循环变量泄露为全局变量


(2)变量提升: var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。


(3)给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。


(4)重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。


(5)暂时性死区: 在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。


(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。


(7)指针指向: let 和 const 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。


Promise.allSettled

描述:等到所有promise都返回结果,就返回一个promise实例。


实现


Promise.allSettled = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return resolve(promises);            let result = [];            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => {                        count++;                        result[index] = {                            status: 'fulfilled',                            value: value                        };                        if(count === promises.length) resolve(result);                    },                     reason => {                        count++;                        result[index] = {                            status: 'rejected'.                            reason: reason                        };                        if(count === promises.length) resolve(result);                    }                );            });        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

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

  • 第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

  • 第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。

  • 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。

手写题:数组去重

Array.from(new Set([1, 1, 2, 2]))
复制代码

代码输出结果

setTimeout(function () {  console.log(1);}, 100);
new Promise(function (resolve) { console.log(2); resolve(); console.log(3);}).then(function () { console.log(4); new Promise((resove, reject) => { console.log(5); setTimeout(() => { console.log(6); }, 10); })});console.log(7);console.log(8);
复制代码


输出结果为:


23784561
复制代码


代码执行过程如下:


  1. 首先遇到定时器,将其加入到宏任务队列;

  2. 遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;

  3. 继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;

  4. 执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;

  5. 执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;

  6. 此时微任务队列为空,继续执行宏任务队列,打印出 1。


做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。

函数防抖

触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。


简单版:函数内部支持使用 this 和 event 对象;


function debounce(func, wait) {    var timeout;    return function () {        var context = this;        var args = arguments;        clearTimeout(timeout)        timeout = setTimeout(function(){            func.apply(context, args)        }, wait);    }}
复制代码


使用:


var node = document.getElementById('layout')function getUserAction(e) {    console.log(this, e)  // 分别打印:node 这个节点 和 MouseEvent    node.innerHTML = count++;};node.onmousemove = debounce(getUserAction, 1000)
复制代码


最终版:除了支持 this 和 event 外,还支持以下功能:


  • 支持立即执行;

  • 函数可能有返回值;

  • 支持取消功能;


function debounce(func, wait, immediate) {    var timeout, result;
var debounced = function () { var context = this; var args = arguments;
if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; };
debounced.cancel = function() { clearTimeout(timeout); timeout = null; };
return debounced;}
复制代码


使用:


var setUseAction = debounce(getUserAction, 10000, true);// 使用防抖node.onmousemove = setUseAction
// 取消防抖setUseAction.cancel()
复制代码

Cookie 有哪些字段,作用分别是什么

Cookie 由以下字段组成:


  • Name:cookie 的名称

  • Value:cookie 的值,对于认证 cookie,value 值包括 web 服务器所提供的访问令牌;

  • Size: cookie 的大小

  • Path:可以访问此 cookie 的页面路径。 比如 domain 是 abc.com,path 是/test,那么只有/test路径下的页面可以读取此 cookie。

  • Secure: 指定是否使用 HTTPS 安全协议发送 Cookie。使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。该方法也可用于 Web 站点的身份鉴别,即在 HTTPS 的连接建立阶段,浏览器会检查 Web 网站的 SSL 证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到 SSL 证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。

  • Domain:可以访问该 cookie 的域名,Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie 受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。

  • HTTP: 该字段包含HTTPOnly 属性 ,该属性用来设置 cookie 能否通过脚本来访问,默认为空,即可以通过脚本访问。在客户端是不能通过 js 代码去设置一个 httpOnly 类型的 cookie 的,这种类型的 cookie 只能通过服务端来设置。该属性用于防止客户端脚本通过document.cookie属性访问 Cookie,有助于保护 Cookie 不被跨站脚本攻击窃取或篡改。但是,HTTPOnly 的应用仍存在局限性,一些浏览器可以阻止客户端脚本对 Cookie 的读操作,但允许写操作;此外大多数浏览器仍允许通过 XMLHTTP 对象读取 HTTP 响应中的 Set-Cookie 头。

  • Expires/Max-size : 此 cookie 的超时时间。若设置其值为一个时间,那么当到达此时间后,此 cookie 失效。不设置的话默认值是 Session,意思是 cookie 会和 session 一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此 cookie 失效。


总结: 服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条 cookie 包括了 5 个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的时间,domain 是域名、path 是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。

代码输出结果

async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");  setTimeout(() => {    console.log('timer1')  }, 0)}async function async2() {  setTimeout(() => {    console.log('timer2')  }, 0)  console.log("async2");}async1();setTimeout(() => {  console.log('timer3')}, 0)console.log("start")
复制代码


输出结果如下:


async1 startasync2startasync1 endtimer2timer3timer1
复制代码


代码的执行过程如下:


  1. 首先进入async1,打印出async1 start

  2. 之后遇到async2,进入async2,遇到定时器timer2,加入宏任务队列,之后打印async2

  3. 由于async2阻塞了后面代码的执行,所以执行后面的定时器timer3,将其加入宏任务队列,之后打印start

  4. 然后执行 async2 后面的代码,打印出async1 end,遇到定时器 timer1,将其加入宏任务队列;

  5. 最后,宏任务队列有三个任务,先后顺序为timer2timer3timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。

为什么 0.1 + 0.2 != 0.3,请详述理由

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。


我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为


// (0011) 表示循环0.1 = 2^-4 * 1.10011(0011)
复制代码


那么如何得到这个二进制的呢,我们可以来演算下


小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)


回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。


所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004


下面说一下原生解决办法,如下代码所示


parseFloat((0.1 + 0.2).toFixed(10))
复制代码

Promise 的基本用法

(1)创建 Promise 对象

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。


Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject


const promise = new Promise(function(resolve, reject) {  // ... some code  if (/* 异步操作成功 */){    resolve(value);  } else {    reject(error);  }});
复制代码


一般情况下都会使用new Promise()来创建 promise 对象,但是也可以使用promise.resolvepromise.reject这两个方法:


  • Promise.resolve


Promise.resolve(value)的返回值也是一个 promise 对象,可以对返回值进行.then 调用,代码如下:


Promise.resolve(11).then(function(value){  console.log(value); // 打印出11});
复制代码


resolve(11)代码中,会让 promise 对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;


创建 promise 对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建 promise 对象;


  • Promise.reject


Promise.reject 也是new Promise的快捷形式,也创建一个 promise 对象。代码如下:


Promise.reject(new Error(“我错了,请原谅俺!!”));
复制代码


就是下面的代码 new Promise 的简单形式:


new Promise(function(resolve,reject){   reject(new Error("我错了!"));});
复制代码


下面是使用 resolve 方法和 reject 方法:


function testPromise(ready) {  return new Promise(function(resolve,reject){    if(ready) {      resolve("hello world");    }else {      reject("No thanks");    }  });};// 方法调用testPromise(true).then(function(msg){  console.log(msg);},function(error){  console.log(error);});
复制代码


上面的代码的含义是给testPromise方法传递一个参数,返回一个 promise 对象,如果为true的话,那么调用 promise 对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用 promise 对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

(2)Promise 方法

Promise 有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。


  1. then()


当 Promise 执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise 创建完了,那该如何调用呢?


promise.then(function(value) {  // success}, function(error) {  // failure});
复制代码


then方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved时调用,第二个回调函数是 Promise 对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then方法后面再调用另一个 then 方法。


当要写有顺序的异步事件时,需要串行时,可以这样写:


let promise = new Promise((resolve,reject)=>{    ajax('first').success(function(res){        resolve(res);    })})promise.then(res=>{    return new Promise((resovle,reject)=>{        ajax('second').success(function(res){            resolve(res)        })    })}).then(res=>{    return new Promise((resovle,reject)=>{        ajax('second').success(function(res){            resolve(res)        })    })}).then(res=>{
})
复制代码


那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。


2. catch()


Promise 对象除了有 then 方法,还有一个 catch 方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。


p.then((data) => {     console.log('resolved',data);},(err) => {     console.log('rejected',err);     }); p.then((data) => {    console.log('resolved',data);}).catch((err) => {    console.log('rejected',err);});
复制代码


3. all()


all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected


javascriptlet promise1 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(1);    },2000)});let promise2 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(2);    },1000)});let promise3 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(3);    },3000)});Promise.all([promise1,promise2,promise3]).then(res=>{    console.log(res);    //结果为:[1,2,3] })
复制代码


调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个 promise 对象resolve执行时的值。


(4)race()


race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected


let promise1 = new Promise((resolve,reject)=>{    setTimeout(()=>{       reject(1);    },2000)});let promise2 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(2);    },1000)});let promise3 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(3);    },3000)});Promise.race([promise1,promise2,promise3]).then(res=>{    console.log(res);    //结果:2},rej=>{    console.log(rej)};)
复制代码


那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:


Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
复制代码


5. finally()


finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。


promise.then(result => {···}).catch(error => {···}).finally(() => {···});
复制代码


上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。


下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。


server.listen(port)  .then(function () {    // ...  })  .finally(server.stop);
复制代码


finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:


promise.finally(() => {  // 语句});// 等同于promise.then(  result => {    // 语句    return result;  },  error => {    // 语句    throw error;  });
复制代码


上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

两栏布局的实现

一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,两栏布局的具体实现:


  • 利用浮动,将左边元素宽度设置为 200px,并且设置向左浮动。将右边元素的 margin-left 设置为 200px,宽度设置为 auto(默认为 auto,撑满整个父元素)。


.outer {  height: 100px;}.left {  float: left;  width: 200px;  background: tomato;}.right {  margin-left: 200px;  width: auto;  background: gold;}
复制代码


  • 利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置 overflow: hidden; 这样右边就触发了 BFC,BFC 的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。


.left{     width: 100px;     height: 200px;     background: red;     float: left; } .right{     height: 300px;     background: blue;     overflow: hidden; }
复制代码


  • 利用 flex 布局,将左边元素设置为固定宽度 200px,将右边的元素设置为 flex:1。


.outer {  display: flex;  height: 100px;}.left {  width: 200px;  background: tomato;}.right {  flex: 1;  background: gold;}
复制代码


  • 利用绝对定位,将父级元素设置为相对定位。左边元素设置为 absolute 定位,并且宽度设置为 200px。将右边元素的 margin-left 的值设置为 200px。


.outer {  position: relative;  height: 100px;}.left {  position: absolute;  width: 200px;  height: 100px;  background: tomato;}.right {  margin-left: 200px;  background: gold;}
复制代码


  • 利用绝对定位,将父级元素设置为相对定位。左边元素宽度设置为 200px,右边元素设置为绝对定位,左边定位为 200px,其余方向定位为 0。


.outer {  position: relative;  height: 100px;}.left {  width: 200px;  background: tomato;}.right {  position: absolute;  top: 0;  right: 0;  bottom: 0;  left: 200px;  background: gold;}
复制代码

深/浅拷贝

首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。


function isObject(obj) {    return typeof obj === "object" && obj !== null;}
复制代码


这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。

如果 new 一个箭头函数的会怎么样

箭头函数是 ES6 中的提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。


new 操作符的实现步骤如下:


  1. 创建一个对象

  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的 prototype 属性)

  3. 指向构造函数中的代码,构造函数中的 this 指向该对象(也就是为这个对象添加属性和方法)

  4. 返回新的对象


所以,上面的第二、三步,箭头函数都是没有办法执行的。

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?

1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题


  1. Vue 使用了 Object.defineProperty 实现双向数据绑定

  2. 在初始化实例时对属性执行 getter/setter 转化

  3. 属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)


所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)


2)接下来我们看看框架本身是如何实现的呢?


Vue 源码位置:vue/src/core/instance/index.js


export function set (target: Array<any> | Object, key: any, val: any): any {  // target 为数组    if (Array.isArray(target) && isValidArrayIndex(key)) {    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // key 已经存在,直接修改属性值    if (key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = (target: any).__ob__  // target 本身就不是响应式数据, 直接赋值  if (!ob) {    target[key] = val    return val  }  // 对属性进行响应式处理  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}
复制代码


我们阅读以上源码可知,vm.$set 的实现原理是:


  1. 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  2. 如果目标是对象,会先判读属性是否存在、对象是否是响应式,

  3. 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理


defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法

说一下类组件和函数组件的区别?

1. 语法上的区别:
函数式组件是一个纯函数,它是需要接受props参数并且返回一个React元素就可以了。类组件是需要继承React.Component的,而且class组件需要创建render并且返回React元素,语法上来讲更复杂。
2. 调用方式
函数式组件可以直接调用,返回一个新的React元素;类组件在调用时是需要创建一个实例的,然后通过调用实例里的render方法来返回一个React元素。
3. 状态管理
函数式组件没有状态管理,类组件有状态管理。
4. 使用场景
类组件没有具体的要求。函数式组件一般是用在大型项目中来分割大组件(函数式组件不用创建实例,所有更高效),一般情况下能用函数式组件就不用类组件,提升效率。
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
京东前端一面面试题_JavaScript_bb_xiaxia1998_InfoQ写作社区