每日一题第二篇

Day71:[手写代码]实现Promise.all方法

// 核心思路
①接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
②这个方法返回一个新的 promise 对象,
③遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
④参数所有回调成功才是成功,返回值数组与参数顺序一致
⑤参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。

// 实现代码
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了~

//代码实现
function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`)
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedResult = [];
    for (let i = 0; i < promiseNum; i++) {
      Promise.resolve(promises[i]).then(value=>{
        resolvedCounter++;
        resolvedResult[i] = value;
        if (resolvedCounter == promiseNum) {
            return resolve(resolvedResult)
          }
      },error=>{
        return reject(error)
      })
    }
  })
}

// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

Day72:有效括号算法题

/*
    给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效
  有效字符串需满⾜:
        1. 左括号必须⽤相同类型的右括号闭合。
    2. 左括号必须以正确的顺序闭合。
  注意空字符串可被认为是有效字符串。
  示例1:
    输⼊: "()"
    输出: true
  示例2:
    输⼊: "()[]{}"
    输出: true
  示例 3:
    输⼊: "(]"
    输出: false
  示例 4:
    输⼊: "([)]"
    输出: false
  示例 5:
    输⼊: "{[]}"
    输出: true
*/
// 思路
1)首先,我们通过上边的例子可以分析出什么样子括号匹配是复合物条件的,两种情况。
    ①第一种(非嵌套情况):{} [] ;
    ②第二种(嵌套情况):{ [ ( ) ] } 。
除去这两种情况都不是符合条件的。
2)然后,我们将这些括号自右向左看做栈结构,右侧是栈顶,左侧是栈尾。
3)如果编译器中的括号是左括号,我们就入栈(左括号不用检查匹配);如果是右括号,就取出栈顶元素检查是否匹配。
4)如果匹配,就出栈。否则,就返回 false;

// 代码实现
var isValid = function(s){
  let stack = [];
  var obj = {
     "[": "]",
     "{": "}",
     "(": ")",
  };
  // 取出字符串中的括号
  for (var i = 0; i < s.length;i++){
    if(s[i] === "[" || s[i] === "{" || s[i] === "("){
      // 如果是左括号,就进栈
      stack.push(s[i]);
    }else{
        var key = stack.pop();
      // 如果栈顶元素不相同,就返回false
      if(obj[key] !== s[i]){
        return false;
      }
    }
  }
  return stack.length ===  0
}

Day73:写出执行结果,并解释原因

function yideng(n,o){
    console.log(o); // ?
    return {
        yideng:function(m){
            return yideng(m,n);
        }
    }
}
const a=yideng(0);a.yideng(1);a.yideng(2);a.yideng(3);
const b=yideng(0).yideng(1).yideng(2).yideng(3);
const c = yideng(0).yideng(1);c.yideng(2);c.yideng(3);
// 答案
undefined 0 0 0 
undefined 0 1 2
undefined 0 1 1

// 解析
闭包知识考查
    return返回的对象的fun属性对应一个新建的函数对象,这个函数对象将形成一个闭包作用域,使其    能够访问外层函数的变量n及外层函数fun,
关键点:
    理清执行的是哪个yideng函数,为了不将yideng函数与yideng属性混淆,等价转换下代码
  function _yideng_(n,o){
      console.log(o);
      return {
          yideng:function(m){
              return _yideng_(m,n);
          }
      }
  }
  const a=_yideng_(0);a.yideng(1);a.yideng(2);a.yideng(3);
  const b=_yideng_(0).yideng(1).yideng(2).yideng(3);
  const c = _yideng_(0).yideng(1).yideng(2);c.yideng(3);

1)第一行a代码执行过程解析,
①const a=_yideng_(0);调用最外层的函数,只传入了n,所以打印o是undefined
②a.yideng(1);调用yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数_yideng_(1,0);所以o为0;
③a.yideng(2);调用yideng(2)时m为2,但依然是调用a.yideng,所以还是闭包了第一次调用时的n,所以内部调用第一层的_yideng_(2,0);所以o为0
④a.yideng(3);同③
所以是undefined 0 0 0

2)第二行b代码执行过程解析
①第一次调用第一层_yideng_(0)时,o为undefined;
②第二次调用 .yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数_yideng_(1,0);所以o为0;
③第三次调用 .yideng(2)时m为2,此时当前的yideng函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层_yideng_(1,0)时,n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层_yideng_函数_yideng_(2,1),所以o为1;
④第四次调用 .yideng(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层_yideng_函数为_yideng_(3,2);所以o为2;
所以是undefined 0 1 2

3)第三行c代码执行过程解析
①在第一次调用第一层_yideng_(0)时,o为undefined;
②第二次调用 .yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数fun(1,0);所以o为0;
③第三次调用 .yideng(2)时m为2,此时yideng闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层_yideng_函数_yideng_(2,1);所以o为1;
④第四次.yideng(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数_yideng_(3,1),所以o还为1
所以是undefined 0 1 1

Day74:写出执行结果,并解释原因

var arr1 = "ab".split('');
var arr2 = arr1.reverse(); 
var arr3 = "abc".split('');
arr2.push(arr3);
console.log(arr1.length);
console.log(arr1.slice(-1));
console.log(arr2.length);
console.log(arr2.slice(-1));
// 答案
3 ["a","b","c"] 3 ["a","b","c"]

//解析
这个题其实主要就是考察的reverse会返回该数组的引用,但是容易被迷惑,导致答错,如果知道这个点,就不会掉坑里了。

1)reverse
MDN 上对于 reverse() 的描述是酱紫的:
The reverse method transposes the elements of the calling array object in place, mutating the array, and returning a reference to the array.
reverse 方法颠倒数组中元素的位置,改变了数组,并返回该数组的引用。

2)slice
slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。

Day75:写出执行结果,并解释原因

var F = function(){}
Object.prototype.a = function(){
console.log('yideng')
}
Function.prototype.b = function(){
console.log('xuetang')
}
var f = new F()
F.a();
F.b();
f.a();
f.b();
// 答案
yideng xuetang yideng 报错
//解析
1)F.a();F.b();
F是个构造函数,而f是构造函数F的一个实例。
因为F instanceof Object == true、F instanceof Function == true
由此我们可以得出结论:F是Object 和 Function两个的实例,即F既能访问到a,也能访问到b。
所以F.a() 输出 yideng F.b() 输出 xuetang

2)f.a();f.b();
对于f,我们先来看下下面的结果:
f并不是Function的实例,因为它本来就不是构造函数,调用的是Function原型链上的相关属性和方法了,只能访问Object原型链。
所以f.a() 输出 yideng 而f.b()就报错了。

3)具体分析下它们是如何按路径查找的:
①f.a的查找路径: f自身: 没有 ---> f.__proto__(Function.prototype),没有--->f.__proto__.__proto__(Object.prototype): 输出yideng
②f.b的查找路径: f自身: 没有 ---> f.__proto__(Function.prototype): 没有 ---> f.__proto__.__proto__ (Object.prototype): 因为找不到,所以报错
③F.a的查找路径: F自身: 没有 ---> F.__proto__(Function.prototype): 没有 ---> F.__proto__.__proto__(Object.prototype): 输出 yideng
④F.b的查找路径: F自身: 没有 ---> F.__proto__(Function.prototype): xuetang

Day76:写出执行结果,并解释原因

const a = [1,2,3],
    b = [1,2,3],
    c = [1,2,4],
        d = "2",
        e = "11";
console.log([a == b, a === b, a > c, a < c, d > e]);
// 答案
[false,false,false,true,true] 

// 解析
1)JavaScript 有两种比较方式:严格比较运算符和转换类型比较运算符。
    对于严格比较运算符(===)来说,仅当两个操作数的类型相同且值相等为 true,而对于被广泛使用的比较运算符(==)来说,会在进行比较之前,将两个操作数转换成相同的类型。对于关系运算符(比如 <=)来说,会先将操作数转为原始值,使它们类型相同,再进行比较运算。
    当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域)时才相等,即他们在栈内存中的引用地址相同。
    javascript中Array也是对象,所以这里a,b,c显然引用是不相同的,所以这里a==b,a===b都为false。

2)两个数组进行大小比较,也就是两个对象进行比较
    当两个对象进行比较时,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法。
①valueOf() 方法返回指定对象的原始值。
  JavaScript调用valueOf方法将对象转换为原始值。你很少需要自己调用valueOf方法;当遇到要预期的原始值的对象时,JavaScript会自动调用它。默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。
②toString() 方法返回一个表示该对象的字符串。
  每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。
③经过valueOf,toString的处理,所以这里a,c最终会被转换为"1,2,3"与"1,2,4";

3)两个字符串进行比较大小
    上边的数组经转换为字符串之后,接着进行大小比较。
    MDN中的描述是这样的:字符串比较则是使用基于标准字典的 Unicode 值来进行比较的。
    字符串按照字典顺序进行比较。JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。
    所以这里 "1,2,3" < "1,2,4",输出true,因为前边的字符的unicode码点都相等,所以最后是比较3和4的unicode码点。而3的Unicode码点是51,4的uniCode码点是52,所以a "11"也是同理,这个也是开发中有时会遇到的问题,所以在进行运算比较时需要注意一下。

4)关于valueOf,toString的调用顺序
①javascript中对象到字符串的转换经历的过程如下:
    如果对象具有toString()方法,javaScript会优先调用此方法。如果返回的是一个原始值(原始值包括null、undefined、布尔值、字符串、数字),javaScript会将这个原始值转换为字符串,并返回字符串作为结果。
    如果对象不具有toString()方法,或者调用toString()方法返回的不是原始值,则javaScript会判断是否存在valueOf()方法,如若存在则调用此方法,如果返回的是原始值,javaScript会将原始值转换为字符串作为结果。
  如果javaScript无法调用toString()和valueOf()返回原始值的时候,则会报一个类型错误异常的警告。
    比如:String([1,2,3]);将一个对象转换为字符串
    
②javaScript中对象转换为数字的转换过程:
    javaScript优先判断对象是否具有valueOf()方法,如具有则调用,若返回一个原始值,javaScript会将原始值转换为数字并作为结果。
    如果对象不具有valueOf()方法,javaScript则会调用toString()的方法,若返回的是原始值,javaScript会将原始值转换为数字并作为结果。
    如果javaScript无法调用toString()和valueOf()返回原始值的时候,则会报一个类型错误异常的警告。
    比如:Number([1,2,3]);将一个对象转换为字符串

Day77:补充代码,使代码可以正确执行

const str = '1234567890';
function formatNumber(str){
  // your code
}
console.log(formatNumber(str)); //1,234,567,890
// 补充代码,使代码可以正确执行
//代码实现
/*
    1.普通版
    优点:比for循环,if-else判断的逻辑清晰直白一些
    缺点:太普通
*/
function formatNumber(str){
  let arr = [],
      count = str.length;
  while(count >= 3){
    // 将字符串3个一组存入数组
    arr.unshift(str.slice(count-3,count));
    count -= 3;
  }
  // 如果不是3的倍数就另外追加到数组
  str.length % 3 && arr.unshift(str.slice(0,str.length % 3));
  return arr.toString();
}
console.log(formatNumber('1234567890'));

/*
    2.进阶版
    优点:JS的API玩的了如之掌
    缺点:可能没那么好懂,但是读懂之后就会发出怎么没想到的感觉
*/
function formatNumber(str){
  //str.split('').reverse() => ["0", "9", "8", "7", "6", "5", "4", "3", "2", "1"]
  return str.split('').reverse().reduce((prev,next,index) => {
    return ((index % 3) ? next : (next + ',')) + prev
  })
}
console.log(formatNumber("1234567890"));

/*
    3.正则版
    优点:代码少,浓缩的都是精华
    缺点:需要对正则表达式的位置匹配有一个较深的认识,门槛大一点
*/
function formatNumber(str) {
  /*
    ①/\B(?=(\d{3})+(?!\d))/g:正则匹配非单词边界\B,即除了1之前的位置,其他字符之间的边界,后面必须跟着3N个数字直到字符串末尾
        ②(\d{3})+:必须是1个或多个的3个连续数字;
        ③(?!\d):第2步中的3个数字不允许后面跟着数字;
  */
  return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
console.log(formatNumber("1234567890")) // 1,234,567,890

/*
    4.Api版
    优点:简单粗暴,直接调用 API
    缺点:Intl兼容性不太好,不过 toLocaleString的话 IE6 都支持
*/
 // ①toLocaleString:方法返回这个数字在特定语言环境下的表示字符串,具体可看MDN描述
function formatNumber(str){
  return Number(str).toLocaleString('en-US');
}
console.log(formatNumber("1234567890"));

 // ②还可以使用IntL对象
// Intl 对象是 ECMAScript 国际化 API 的一个命名空间,它提供了精确的字符串对比,数字格式化,日期和时间格式化。Collator,NumberFormat 和 DateTimeFormat 对象的构造函数是 Intl 对象的属性。
function formatNumber(str){
  return new Intl.NumberFormat().format(str);
}
console.log(formatNumber("1234567890"));

Day79:写出下面代码null和0进行比较的代码执行结果,并解释原因

console.log(null == 0);
console.log(null <= 0);
console.log(null < 0);

答案

false true false

解析

  1. 在JavaScript中,null不等于零,也不是零。
  2. null只等于undefined 剩下它俩和谁都不等
  3. 关系运算符,在设计上总是需要运算元尝试转为一个number,而相等运算符在设计上,则没有这方面的考虑。所以 计算null<=0 或者>=0的时候回触发Number(null),它将被视为0(Number(null) == 0为true)

Day80:关于数组sort,下面代码的正确打印结果是什么,并解释原因

const arr1 = ['a', 'b', 'c'];
const arr2 = ['b', 'c', 'a'];
console.log(
  arr1.sort() === arr1,
  arr2.sort() == arr2,
  arr1.sort() === arr2.sort()
);

答案

true, true, false

解析

  1. array 的 sort 方法对原始数组进行排序,并返回对该数组的引用。调用 arr2.sort() 时,arr2 数组内的对象将会被排序。
  2. 当你比较对象时,数组的排序顺序并不重要。由于 arr1.sort() 和 arr1 指向内存中的同一对象,因此第一个相等测试返回 true。第二个比较也是如此:arr2.sort() 和 arr2 指向内存中的同一对象。
  3. 在第三个测试中,arr1.sort() 和 arr2.sort() 的排序顺序相同;但是,它们指向内存中的不同对象。因此,第三个测试的评估结果为 false。

Day81:介绍防抖与节流的原理,并动手实现

const debounce = (fn,delay) => {
  // 介绍防抖函数原理,并实现
  // your code
}
const throttle = (fn,delay = 500) => {
  // 介绍节流函数原理,并实现
   // your code
}

答案与解析

1)防抖函数

防抖函数原理:

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

适用场景:

  1. 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  2. 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
// 手写简化版实现
const debounce = (fn,delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this,args);
    },delay)
  }
}
2)节流函数

节流函数原理:

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次

适用场景:

  1. 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  2. 缩放场景:监控浏览器resize
  3. 动画场景:避免短时间内多次触发动画引起性能问题
// 手写简化版实现
// ①定时器实现
const throttle = (fn,delay = 500) =>{
  let flag = true;
  return (...args) => {
    if(!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this,args);
      flag = true;
    },delay);
  };
}
// ②时间戳实现
const throttle = (fn,delay = 500) => {
  let preTime = Date.now();
  return (...args) => {
    const nowTime = Date.now();
    if(nowTime - preTime >= delay){
        preTime = Date.now();
        fn.apply(this,args);
    }
  }
}

Day82:关于隐式转换,下面代码的执行结果是什么?并解释原因

let a = [];
let b = "0";
console.log(a == 0);
console.log(a == !a);
console.log(b == 0);
console.log(a == b);

答案

true true true false

解析

1)[] == 0 => true

对象与原始类型值相等比较,对象类型会依照ToPrimitive规则转换成原始类型的值再进行比较。

①[] == 0 =>[].valueOf().toSting() == 0 => '' == 0

数组[]是对象类型,所以会进行ToPrimitive操作,即调用valueOf再调用toString,数组被转换为空字符串'',

②'' == 0 => Number('') == 0 => 0 == 0 => true

空字符串再和数字0比较时,比较的是原始类型的值,原始类型的值会转成数值再进行比较,所以最后得到true

2)[] == ![] => true

!的优先级高于==,所以先执行!,将[]转为boolean值,null、undefined、NaN以及空字符串('')取反都为true,其余都为false,所以![]为false

[] == false => 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值 => [] == 0 => 同第一问 => true

3)"0" == 0 => true

如果比较的是原始类型的值,原始类型的值会转成数值再进行比较

Number('0') => 0 => 0 == 0 => true

4)[] == "0" => false

根据1)可以知道[] 被转换为了 '' , 所以'' == '0',为false

知识点

1.ToString、ToNumber、ToBoolean、ToPrimitive转换规则:

1)ToString

这里所说的ToString可不是对象的toString方法,而是指其他类型的值转换为字符串类型的操作。

看下null、undefined、布尔型、数字、数组、普通对象转换为字符串的规则:

  1. null:转为"null"
  2. undefined:转为"undefined"
  3. 布尔类型:true和false分别被转为"true"和"false"
  4. 数字类型:转为数字的字符串形式,如10转为"10", 1e21转为"1e+21"
  5. 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的null或undefined,会被当做空字符串处理
  6. 普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"

2)ToNumber

ToNumber指其他类型转换为数字类型的操作。

  1. null: 转为0
  2. undefined:转为NaN
  3. 字符串:如果是纯数字形式,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN
  4. 布尔型:true和false被转为1和0
  5. 数组:数组首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理,
  6. 对象:同数组的处理

3)ToBoolean

ToBoolean指其他类型转换为布尔类型的操作

js中的假值只有false、null、undefined、空字符、0和NaN,其它值转为布尔型都为true。

4)ToPrimitive

ToPrimitive指对象类型类型(如:对象、数组)转换为原始类型的操作。

  1. 当对象类型需要被转为原始类型时,它会先查找对象的valueOf方法,如果valueOf方法返回原始类型的值,则ToPrimitive的结果就是这个值
  2. 如果valueOf不存在或者valueOf方法返回的不是原始类型的值,就会尝试调用对象的toString方法,也就是会遵循对象的ToString规则,然后使用toString的返回值作为ToPrimitive的结果。如果valueOf和toString都没有返回原始类型的值,则会抛出异常。
  3. 注意:对于不同类型的对象来说,ToPrimitive的规则有所不同,比如Date对象会先调用toString,

ECMA规则:https://www.ecma-international.org/ecma-262/6.0/#sec-toprimitive

  1. Number([]), 空数组会先调用valueOf,但返回的是数组本身,不是原始类型,所以会继续调用toString,得到空字符串,相当于Number(''),所以转换后的结果为"0"
  2. 同理,Number(['10'])相当于Number('10'),得到结果10

2.宽松相等(==)比较时的隐士转换规则

宽松相等(==)和严格相等(===)的区别在于宽松相等会在比较中进行隐式转换。

  1. 布尔类型和其他类型的相等比较,只要布尔类型参与比较,该布尔类型的值首先会被转换为数字类型
  2. 数字类型和字符串类型的相等比较,当数字类型和字符串类型做相等比较时,字符串类型会被转换为数字类型
  3. 当对象类型和原始类型做相等比较时,对象类型会依照ToPrimitive规则转换为原始类型
  4. 当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域)时才相等,即他们在栈内存中的引用地址相同。
  5. ECMAScript规范中规定null和undefined之间互相宽松相等(==),并且也与其自身相等,但和其他所有的值都不宽松相等(==)

Day83:请写出如下代码的打印结果,并解释为什么

var obj = {};
var x = +obj.yideng?.name ?? '京程一灯';
console.log(x);

答案

NaN

解析

  1. ?省去过去判断key的麻烦。所以 obj.yideng?.name 遇到不存在的值返回undefined
  2. +undefined 强制转化number NaN
  3. NaN ?? 京程一灯 返回NaN。原因:??为空值合并操作符(是一个逻辑操作符,当左侧的表达式结果为 null 或者 undefined 时,其返回右侧表达式的结果,否则返回左侧表达式的结果。
  4. 参考链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

Day84:对于length下面代码的输出结果是什么?并解释原因

 function foo(){
   console.log(length);
 }
function bar(){
  var length = "京程一灯";
  foo();
}
bar();

答案

0 (页面iframe数量)

解析

  1. 首次运行执行foo,foo内寻找length并没有定义
  2. 然后很多同学可能会觉得在bar内定义了length。foo内应该寻找到了length
  3. 其实函数作用域是在执行函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了。所以向上寻找到该是全局的length
  4. 那你会觉得length应该是undefined。其实length是你页面iframe的数量

Day85:对于扩展运算符,下面代码的执行结果是什么?并解释原因

let ydObject = { ...null, ...undefined };
console.log(ydObject);
let ydArray = [...null, ...undefined];
console.log(ydArray);

答案

{}  抛出异常

解析

对象会忽略 null 和 undefined,数组会抛异常。这是ECMA的规范定义,所以大家在使用扩展运算符的时候还是要多加注意。这里补充一个其他小知识点,null 只能等于undefined,其余谁也不等。

Day86:写出类数组转换结果,并解释原因

const arrLike = {
  length:4,
  0:0,
  1:1,
  '-1':2,
  3:3,
  4:4,
}
console.log(Array.from(arrLike));
console.log(Array.prototype.slice.call(arrLike));

答案

[0,1,undefined,3]
[0,1,empty,3]

解析

1)类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。

2)类数组转换为数组的方式

  1. 使用 Array.from()
  2. 使用 Array.prototype.slice.call()
  3. 使用 Array.prototype.forEach() 进行属性遍历并组成新的数组

3)转换须知

  1. 转换后的数组长度由 length 属性决定。索引不连续时转换结果是连续的,会自动补位。
  2. 仅考虑 0或正整数 的索引
  3. 使用slice转换产生稀疏数组

4)扩展

稀疏数组是指索引不连续,数组长度大于元素个数的数组,通俗地说就是 有空隙的数组。

empty vs undefined

①稀疏数组在控制台中的表示:

var a = new Array(5);
console.log(a);    // [empty × 5]

这里表示数组 a 有5个空隙。 empty 并非 JS 的基础数据类型

访问数组元素:a[0]; // undefined

②empty 和 undefined 不是一个含义

var b = [undefined, undefined, undefined];
console.log(b);    // [undefined, undefined, undefined]
b[0];              // undefined

a.forEach(i => { console.log(i) });    // 无 log 输出
b.forEach(i => { console.log(i) });    // undefined undefined undefined

数组 a 和 数组 b 只有访问具体元素的时候输出一致,其他情况都是存在差异的。遍历数组 a 时,由于数组中没有任何元素,所以回调函数不执行不会有 log 输出;而遍历数组 b 时,数组其实填充着元素 undefined,所以会打印 log。

这里的数组 b 其实是一个 密集数组。

为什么访问稀疏数组的缺失元素时会返回 undefined,是因为 JS 引擎在发现元素缺失时会临时赋值 undefined,类似于 JS 变量的声明提升:

console.log(a); // undefined
var a = 0;

3)稀疏数组跟密集数组相比具有以下特性:

  1. 访问速度慢
  2. 内存利用率高

4)稀疏数组跟密集数组相比访问速度慢的原因

  1. 该特性与 V8 引擎构建 JS 对象的方式有关。V8 访问对象有两种模式:字典模式 和 快速模式。
  2. 稀疏数组使用的是字典模式,也称为 散列表模式,该模式下 V8 使用散列表来存储对象属性。由于每次访问时都需要计算哈希值(实际上只需要计算一次,哈希值会被缓存)和寻址,所以访问速度非常慢。另一方面,对比起使用一段连续的内存空间来存储稀疏数组,散列表的方式会大幅度地节省内存空间。
  3. 而密集数组在内存空间中是被存储在一个连续的类数组里,引擎可以直接通过数组索引访问到数组元素,所以速度会非常快。

Day87:写出下面代码1,2,3的大小判断结果

console.log(1 < 2 < 3);
console.log(3 > 2 > 1);

答案

true false

解析

  1. 对于运算符>、<,一般的计算从左向右
  2. 第一个题:1 < 2 等于 true, 然后true < 3,true == 1 ,因此结果是true
  3. 第二个题:3 > 2 等于 true, 然后true > 1, true == 1 ,因此结果是false

Day88:以下两段代码会抛出异常吗?解释原因?

let yd = { x: 1, y: 2 };
// 以下两段代码会抛出异常吗?
let ydWithXGetter1 = {
  ...yd,
  get x() {
    throw new Error();
  },
};

let ydWithXGetter2 = {
  ...yd,
  ...{
    get x() {
      throw new Error();
    },
  },
};

答案

ydWithXGetter1不会报错 ydWithXGetter2会

解析

// 第一段代码实际等价于如下代码,所以不会报错
let ydWithXGetter1  = {};
Object.assign(ydWithXGetter1, yd);
Object.defineProperty(ydWithXGetter1, "x", {
  get(){ throw new Error() },
  enumerable : true,
  configurable : true
});
// 第二段代码会报错实际解构如下代码时.x被调了
// 原因是读取一个属性的时候会去对象的[[get]]中查找是否有该属性名
...{ get x() { throw new Error() } }

Day89:请问React调用机制一共对任务设置了几种优先级别?每种优先级都代表的具体含义是什么?在你开发过程中如果遇到影响主UI渲染卡顿的任务,你又是如何利用这些优先级的?
null

答案

//React 一共有这么6种任务的优先级。
//初始化和重置root和占位用的
export const NoPriority = 0;
//立即执行的优先级 一般用来执行过期的任务
export const ImmediatePriority = 1;
//会阻塞渲染的优先级别,用户和页面交互用的
export const UserBlockingPriority = 2;
//默认优先级 普通的优先级别
export const NormalPriority = 3;
//低优先级别(用户可使用)
export const LowPriority = 4;
//空闲优先级 用户不在意的任务(用户可使用)
export const IdlePriority = 5;

/*
* 开发中怎么使用这些优先级呢?
* Concurrent在目前的React版本中还是实验性的,
* 也请大家及时关注我们的公开课和对新版本的开箱测评
*/
//理想化
React.unstable_scheduleCallback(priorityLevel, callback, { timeout:  })
//现实
ReactDOM.createRoot( document.getElementById('container') ).render(  )

Day90:Vue父组件可以监听到子组件的生命周期吗?如果能请写出你的实现方法。
null

答案

可以

1)实现方式一

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue

    
// Child.vue
mounted() {
  this.$emit("mounted");
}

2)实现方式二

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue


doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
    
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},  

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

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

Vue.set (object, propertyName, value) 
vm.$set (object, propertyName, value)

答案

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 | 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 的功能所调用的方法

Day92:既然 Vue 通过数据劫持可以精准探测数据在具体dom上的变化,为什么还需要虚拟 DOM diff 呢?
null

答案

前置知识: 依赖收集、虚拟 DOM、响应式系统

现代前端框架有两种方式侦测变化,一种是 pull ,一种是 push

pull: 其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用setStateAPI显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。

push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知。因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher(具体如何创建的Watcher可以先了解下Vue双向数据绑定的原理如下图)

vue 双向数据绑定原理

一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的。

Day93:Vue组件中写name选项有除了搭配keep-alive还有其他作用么?你能谈谈你对keep-alive了解么?(平时使用和源码实现方面)
null

答案

一、组件中写 name 选项有什么作用?

  1. 项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
  2. DOM 做递归组件时需要调用自身 name
  3. vue-devtools 调试工具里显示的组见名称是由vue中组件name决定的

二、keep-alive使用

  1. keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染
  2. 一般结合路由和动态组件一起使用,用于缓存组件;
  3. 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  4. 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

三、keep-alive实现原理

1)首先看下源码

// 源码位置:src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键

  props: {
    include: patternTypes, // 缓存白名单
    exclude: patternTypes, // 缓存黑名单
    max: [String, Number] // 缓存的组件实例数量上限
  },

  created () {
    this.cache = Object.create(null) // 缓存虚拟dom
    this.keys = [] // 缓存的虚拟dom的健集合
  },

  destroyed () {
    for (const key in this.cache) { // 删除所有的缓存
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 实时监听黑白名单的变动
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // .....
  }
}

大概的分析源码,我们发现与我们定义组件的过程一样,先是设置组件名为keep-alive,其次定义了一个abstract属性,值为true。这个属性在vue的官方教程并未提及,其实是一个虚组件,后面渲染过程会利用这个属性。props属性定义了keep-alive组件支持的全部参数。

2)接下来重点就是keep-alive在它生命周期内定义了三个钩子函数了

created

初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合

destroyed

删除缓存VNode还要对应执行组件实例的destory钩子函数。

删除this.cache中缓存的VNode实例。不是简单地将this.cache置为null,而是遍历调用pruneCacheEntry函数删除。

// src/core/components/keep-alive.js
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy() // 执行组件的destory钩子函数
  }
  cache[key] = null
  remove(keys, key)
}

mounted

在mounted这个钩子中对include和exclude参数进行监听,然后实时地更新(删除)this.cache对象数据。pruneCache函数的核心也是去调用pruneCacheEntry。

3)render

// src/core/components/keep-alive.js
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
    // check pattern
    const name: ?string = getComponentName(componentOptions) // 组件名
    const { include, exclude } = this
    if ( // 条件匹配
    // not included
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
    ) {
    return vnode
    }

    const { cache, keys } = this
    const key: ?string = vnode.key == null // 定义组件的缓存key
    // same constructor may get registered as different local components
    // so cid alone is not enough (#3269)
    ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
    : vnode.key
    if (cache[key]) { // 已经缓存过该组件
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    remove(keys, key)
    keys.push(key) // 调整key排序
    } else {
    cache[key] = vnode // 缓存组件对象
    keys.push(key)
    // prune oldest entry
    if (this.max && keys.length > parseInt(this.max)) { 
        // 超过缓存数限制,将第一个删除(LRU缓存算法)
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
    }

    vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
  • 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
  • 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  • 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  • 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。
  • 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

最后就是再次渲染执行缓存和对应钩子函数了

Day94:说一下React Hooks在平时开发中需要注意的问题和原因?
null
1)不要在循环,条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook

这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。

2)使用useState时候,使用push,pop,splice等直接更改数组对象的坑

使用push直接更改数组无法获取到新值,应该采用析构方式,但是在class里面不会有这个问题

代码示例

function Indicatorfilter() {
  let [num,setNums] = useState([0,1,2,3])
  const test = () => {
    // 这里坑是直接采用push去更新num
    // setNums(num)是无法更新num的
    // 必须使用num = [...num ,1]
    num.push(1)
    // num = [...num ,1]
    setNums(num)
  }
return (
    
测试
{num.map((item,index) => (
{item}
))}
) } class Indicatorfilter extends React.Component{ constructor(props:any){ super(props) this.state = { nums:[1,2,3] } this.test = this.test.bind(this) } test(){ // class采用同样的方式是没有问题的 this.state.nums.push(1) this.setState({ nums: this.state.nums }) } render(){ let {nums} = this.state return(
测试
{nums.map((item:any,index:number) => (
{item}
))}
) } }

3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect

看下面的例子

TableDeail是一个公共组件,在调用它的父组件里面,我们通过set改变columns的值,以为传递给TableDeail的columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新

const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
}

// 正确的做法是通过useEffect改变这个值
const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
    useEffect(() =>{setTabColumn(columns)},[columns])
}

4)善用useCallback

父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。

5)不要滥用useContent

可以使用基于useContent封装的状态管理工具。

Day95:Promise.all中任何一个Promise出现错误的时候都会执行reject,导致其它正常返回的数据也无法使用。你有什么解决办法么?
null
1)在单个的catch中对失败的promise请求做处理

2)把reject操作换成 resolve(new Error("自定义的error"))

3)引入Promise.allSettled

const promises = [
    fetch('/api1'),
    fetch('/api2'),
    fetch('/api3'),
  ];
  
  Promise.allSettled(promises).
    then((results) => results.forEach((result) => console.log(result.status)));
  // "fulfilled"
  // "fulfilled"
  // "rejected"

4)安装第三方库 promise-transaction

// 它是promise事物实现 不仅仅能处理错误还能回滚
  import Transaction from 'promise-transaction';
const t = new Transaction([
  {
    name: 'seed',
    perform: () => Promise.resolve(3),
    rollback: () => false,
    retries: 1, // optionally you can define how many retries you like to run if initial attemp fails for this step
  },
  {
    name: 'square',
    perform: (context) => {
      return Promise.resolve(context.data.seed * context.data.seed);
    },
    rollback: () => false,
  },
]);
 
return t.process().then((result) => {
  console.log(result); // should be value of 9 = 3 x 3
});

Day96:请能尽可能多的说出 Vue 组件间通信方式?在组件的通信中EventBus非常经典,你能手写实现下EventBus么?
null

一、Vue组件通信方式

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练

Vue 组件间通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

1.props / $emit

适用于父子组件通信

这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

2.ref$parent / $children

适用于父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

3.EventBus ($emit / $on)

适用于父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

4.$attrs/$listeners

适用于隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

5.provide / inject

适用于隔代组件通信

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

6.Vuex

适用于父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

二、手写实现简版EventBus

// 组件通信,一个触发与监听的过程
class EventEmitter {
    constructor () {
      // 存储事件
      this.events = this.events || new Map()
    }
    // 监听事件
    addListener (type, fn) {
      if (!this.events.get(type)) {
        this.events.set(type, fn)
      }
    }
    // 触发事件
    emit (type) {
      let handle = this.events.get(type)
      handle.apply(this, [...arguments].slice(1))
    }
  }
  // 测试
  let emitter = new EventEmitter()
  // 监听事件
  emitter.addListener('ages', age => {
    console.log(age)
  })
  // 触发事件
  emitter.emit('ages', 18)  // 18

Day97:请讲一下react-redux的实现原理?
null

实现原理

redux

1.Provider

Provider的作用是从最外部封装了整个应用,并向connect模块传递store

2.connect

负责连接React和Redux

1)获取state

connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state

2)包装原组件

将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent

3)监听store tree变化

connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

Day98:写出下面代码的执行结果,并解释原因

Object.prototype.yideng = "京程一灯";
var a = 123;
a.b = 456;
console.log(a.yideng);
console.log(a.b)

答案

京程一灯  undefined

解析

1.“JS中一切皆对象”说法也对也不对

因为实际上JS中包括两种类型的值:

  • 基本类型:包括数值类型、字符串类型、布尔类型等等
  • 对象类型

说它对,是因为在某些情况下,基本类型会表现的很像对象类型,使得用户可以像使用对象去使用基本类型数据。这里的某些情况主要是指 对对象的赋值和读取

以数值为例

var a = 123.1;
console.log(a.toFixed(3)); // 123.100

a.name = 'yideng';
console.log(a.name); //undefined

上边例子,说明基本类型可以像对象类型一样使用,包括访问其属性、对象属性赋值(尽管实际上不起作用,但是形式上可以)。

像题目中的这种赋值

a.b = 456;

结果取决于a的类型:

  1. 如果值a的类型为Undefined或Null,则会抛出错误,
  2. 如果 a 的值是Object类型,那么 b 将在该对象上定义一个命名属性 a(如果需要),并且其值将被设置为 456
  3. 如果值 a 的类型是数字,字符串或布尔值,那么变量 a 将不会以任何方式改变。在这种情况下,上述分配操作将成为noop。

noop

所以,正如你所看到的,如果这些变量是对象,那么将属性赋值给变量才有意义。如果情况并非如此,那么这项任务根本就什么也不做,甚至会出错。

2.为什么会出现这样的情况

之所以可以这样去使用基本类型,是因为JavaScript引擎内部在处理对某个基本类型 a 进行形如 a.xxx 的操作时,会在内部临时创建一个对应的包装类型(对数字类型来说就是Number类型)的临时对象,并把对基本类型的操作代理到对这个临时对象身上,使得对基本类型的属性访问看起来像对象一样。但是在操作完成后,临时对象就扔掉了,下次再访问时,会重新建立临时对象,当然对之前的临时对象的修改都不会有效了。

专业点的解释就是:由于自动装箱(更具体地说,是ECMA-262第5版第8.7.2节中描述的算法),将属性分配给基本类型是完全有效的。但是,属性将被添加到纯临时包装器对象而不是基本类型,因此无法获取属性(包装器对象不替换基本类型);

除非赋值有副作用(例如,如果属性是通过访问函数实现的)

var a= 123;
Object.defineProperty( Number.prototype, 'yideng', {
    get: () => {
       return  '京程一灯';
    }
}); 
console.log(a.yideng);  //京程一灯
// 或者
Object.prototype.yideng = "京程一灯";
console.log(a.yideng);  //京程一灯

关于prototype如果还有不明白的,回顾下这张图

prototype

Day99:React 中 setState 后发生了什么?setState 为什么默认是异步?setState 什么时候是同步?
null

一、React中setState后发生了什么

在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。

经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。

在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。

在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

二、setState 为什么默认是异步

假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnode diff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。

三、setState 什么时候是同步

在setTimeout或者原生事件中,setState是同步的。

Day100:哪些方法会触发 react 重新渲染?重新渲染 render 会做些什么?
null

一、哪些方法会触发 react 重新渲染?

1.setState() 方法被调用

setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候一定会重新渲染吗?

答案是不一定。当 setState 传入 null 的时候,并不会触发 render。

class App extends React.Component {
  state = {
    a: 1
  };

  render() {
    console.log("render");
    return (
      
        

{this.state.a}

); } }

2.父组件重新渲染

只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render。

3.forceUpdate()

默认情况下,当组件的state或props改变时,组件将重新渲染。如果你的render()方法依赖于一些其他的数据,你可以告诉React组件需要通过调用forceUpdate()重新渲染。

调用forceUpdate()会导致组件跳过shouldComponentUpdate(),直接调用render()。这将触发组件的正常生命周期方法,包括每个子组件的shouldComponentUpdate()方法。

forceUpdate就是重新render。有些变量不在state上,当时你又想达到这个变量更新的时候,刷新render;或者state里的某个变量层次太深,更新的时候没有自动触发render。这些时候都可以手动调用forceUpdate自动触发render

二、重新渲染 render 会做些什么?

  1. 会对新旧 VNode 进行对比,也就是我们所说的DoM diff。
  2. 对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
  3. 遍历差异对象,根据差异的类型,根据对应对规则更新VNode

React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法 ,但是这个过程仍然会损耗性能。

三、总结

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以我们的业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。

这里提下优化的点

1.shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

2.利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

3.使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件 。

4.合理拆分组件

微服务的核心思想是:以更轻、更小的粒度来纵向拆分应用,各个小应用能够独立选择技术、发展、部署。我们在开发组件的过程中也能用到类似的思想。试想当一个整个页面只有一个组件时,无论哪处改动都会触发整个页面的重新渲染。在对组件进行拆分之后,render 的粒度更加精细,性能也能得到一定的提升。

Day101:Vue v-model 是如何实现的,语法糖实际是什么
null

一、语法糖

指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。糖在不改变其所在位置的语法结构的前提下,实现了运行时的等价。可以简单理解为,加糖后的代码编译后跟加糖前一样,代码更简洁流畅,代码更语义自然.

二、实现原理

1.作用在普通表单元素上

动态绑定了 inputvalue 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值


//  等同于

//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

2.作用在组件上

在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件

本质是一个父子组件通信的语法糖,通过prop和$.emit实现

因此父组件v-model语法糖本质上可以修改为 ''

在组件的实现中,我们是可以通过 v-model属性 来配置子组件接收的prop名称,以及派发的事件名称。

例子

// 父组件

// 等价于


// 子组件:


props:{value:aa,}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}

默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event

但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

js 监听input 输入框输入数据改变,用oninput ,数据改变以后就会立刻出发这个事件。

通过input事件把数据$emit 出去,在父组件接受。

父组件设置v-model的值为input$emit过来的值。

Day102:说一下减少 dom 数量的办法?一次性给你大量的 dom 怎么优化?
null

一、减少DOM数量的方法

  1. 可以使用伪元素,阴影实现的内容尽量不使用DOM实现,如清除浮动、样式实现等;
  2. 按需加载,减少不必要的渲染;
  3. 结构合理,语义化标签;

二、大量DOM时的优化

当对Dom元素进行一系列操作时,对Dom进行访问和修改Dom引起的重绘和重排都比较消耗性能,所以关于操作Dom,应该从以下几点出发:

1.缓存Dom对象

首先不管在什么场景下。操作Dom一般首先会去访问Dom,尤其是像循环遍历这种时间复杂度可能会比较高的操作。那么可以在循环之前就将主节点,不必循环的Dom节点先获取到,那么在循环里就可以直接引用,而不必去重新查询。

let rootElem = document.querySelector('#app');
let childList = rootElem.child; // 假设全是dom节点
for(let i = 0;i

2.文档片段

利用document.createDocumentFragment()方法创建文档碎片节点,创建的是一个虚拟的节点对象。向这个节点添加dom节点,修改dom节点并不会影响到真实的dom结构。

我们可以利用这一点先将我们需要修改的dom一并修改完,保存至文档碎片中,然后用文档碎片一次性的替换真是的dom节点。与虚拟dom类似,同样达到了不频繁修改dom而导致的重排跟重绘的过程。

let fragment = document.createDocumentFragment();
const operationDomHandle = (fragment) =>{
    // 操作 
}
operationDomHandle(fragment);
// 然后最后再替换  
rootElem.replaceChild(fragment,oldDom);

这样就只会触发一次回流,效率会得到很大的提升。如果需要对元素进行复杂的操作(删减、添加子节点),那么我们应当先将元素从页面中移除,然后再对其进行操作,或者将其复制一个(cloneNode()),在内存中进行操作后再替换原来的节点。

var clone=old.cloneNode(true);
operationDomHandle(clone);
rootElem.replaceChild(clone,oldDom)

3.用innerHtml 代替高频的appendChild

4.最优的layout方案

批量读,一次性写。先对一个不在render tree上的节点进行操作,再把这个节点添加回render tree。这样只会触发一次DOM操作。 使用requestAnimationFrame(),把任何导致重绘的操作放入requestAnimationFrame

5.虚拟Dom

js模拟DOM树并对DOM树操作的一种技术。virtual DOM是一个纯js对象(字符串对象),所以对他操作会高效。

利用virtual dom,将dom抽象为虚拟dom,在dom发生变化的时候先对虚拟dom进行操作,通过dom diff算法将虚拟dom和原虚拟dom的结构做对比,最终批量的去修改真实的dom结构,尽可能的避免了频繁修改dom而导致的频繁的重排和重绘。

你可能感兴趣的:(每日一题第二篇)