《前端100问》40

31、改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

for (var i = 0; i< 10; i++){
	setTimeout(() => {
		console.log(i);
    }, 1000)
}

思路就是:形成词法环境嵌套,形成闭包,延长变量生命

//let 会形成块级作用域,此时 for 类比于 function 内层函数使用了外层函数的变量 闭包
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

//setTimeout(fucntion,time,...arg) arg为传给 function 的参数
for (var i = 0; i < 10; i++) {
  setTimeout(
    i => {
      console.log(i)
    },
    1000,
    i
  )
}

// 解法二:闭包
for (var i = 0; i < 10; i++) {
  (i => {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  })(i)
}


for (var i = 0; i < 10; i++) {
  setTimeout(
    (i => {
      console.log(i)
    })(i),
    1000
  )
}

32、Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

Virtual DOM 在某些情况下,并不一定比操作原生 DOM 快,这是一个性能和可维护性的取舍

没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。

框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。

性能比较也要看场合

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

33、下面的代码打印什么内容,为什么?

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();

打印:

function b(){
    b = 20;
    console.log(b); 
}

原因:

  1. 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
  2. 对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
  3. IIFE中的函数是函数表达式,而不是函数声明。
let function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
   statements
};

//函数表达式 函数吗 【name】只在函数体内有效

所以:

var b = 10;

//IIFE中的函数是函数表达式,而不是函数声明。
//函数名只在该函数内部有效,而且 b是常量绑定,不可以更改
(function b(){ 
  //对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
    b = 20; //非严格模式 b = 20失败
    console.log(b); 
})();
//所以此时b = function b(){b=20;console.log(b)},自然打印出这个

34、简单改造下面的代码,使之分别打印 10 和 20。

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();
//打印20
var b = 10;
//去掉函数名
(function (){
    b = 20;
    console.log(b); 
})();
//打印10
var b = 10;
(function (){
    console.log(b); 
})();

35、浏览器缓存读取规则

可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?

如果 Service Worker 存在,可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件

如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据

缓存分为强缓存和协商缓存,如果命中强缓存则

新开一个以前打开过的页面缓存会从 Disk Cache 中拿

刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是 从Disk Cache中拿

哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?

对于大文件来说,大概率是不存储在内存中的,反之优先
当前系统内存使用率高的话,文件优先存储进硬盘

页面上已经下载的样式、脚本、图片等存在 Memory Cache 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

36、使用迭代的方式实现 flatten 函数。

将一个多维数组拍平为一个一维数组

[1,2,3,[4,5],[6,[7,[8]]]]

如何判断是否为数组:
Object.prototype.toString.call() “[object Array]”
instanceof Array
Array.isArray()
就性能来说 Array.isArray 的性能最好,instanceofObject.prototype.toString.call() 稍微好了一点点

const example = [1, 2, 3, [4, 5], [6, [7, [8]]]]

function flatten(arr) {
  let flatArr = []
  const walk = arr => {
    for (const item of arr) {
      if (Array.isArray(item)) {
          flatArr.concat(walk(item))
      } else {
        flatArr.push(item)
      }
    }
  }
  walk(arr)
 return flatArr
}

console.log(flatten(example))

37、为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?

Redux的设计参考了 Flux的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个Redux都是函数式编程的范式,要求reducer是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。所以Redux有三大原则:

  • 单一数据源,也就是state
  • state 是只读,Redux并没有暴露出直接修改state的接口,必须通过action来触发修改
  • 使用纯函数来修改state,reducer必须是纯函数

同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。

如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。其实我有个点子一直没时间做,那就是把记录下来的 mutations 做成类似 rx-marble 那样的时间线图,对于理解应用的异步状态变化很有帮助

一句话:为了devtools 开发工具更好的记录追踪 应用状态如何变化,更好的进行 time travel

38、下面代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}

==操作会隐式类型转换,即的执行一次toString()方法

引用类型在比较运算符时候,隐式转换会调用本类型 toString 或 valueOf 方法.

改写 a 的 toString()方法

var a = {
  i: 1,
  toString() {
    return a.i++
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}

39、介绍下 BFC 及其应用。

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:

  1. html 根元素
  2. float 浮动
  3. 绝对定位
  4. overflow 不为 visiable
  5. display 为表格布局或者弹性布局

BFC 主要的作用是:

  1. 清除浮动
  2. 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

BFC特性:

  1. 内部box会在垂直方向,一个接一个地放置。
  2. Box垂直方向的距离由margin决定,在一个BFC中,两个相邻的块级盒子的垂直外边距会产生折叠。
  3. 在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左边缘(border-left)(对于从右到左的格式来说,则触碰到右边缘)
  4. 形成了BFC的区域不会与float box重叠
  5. 计算BFC高度时,浮动元素也参与计算

40、在 Vue 中,子组件为何不可以修改父组件传递的 Prop

如果修改了,Vue 是如何监控到属性的修改并给出警告的。

  1. 单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。
  2. 每当父组件属性值修改时,该值都将被覆盖;如果要有不同的改变,可以用基于 prop 的 data 或者 computed

在 initProps 的时候,在 defineReactive 时通过判断是否在开发环境,如果是开发环境,会在触发 set 的时候判断是否此 key 是否处于 updatingChildren 中被修改,如果不是,说明此修改来自子组件,触发 warning 提示。

if (process.env.NODE_ENV !== 'production') {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }

在组件 initProps 方法的时候,会对 props 进行 defineReactive 操作,传入的第四个参数是自定义的 set 函数,该函数会在触发 props 的 set 方法时执行,当 props 修改了,就会运行这里传入的第四个参数,然后进行判断,如果不是 root 根组件,并且不是更新子组件,那么说明更新的是 props,所以会警告

// src/core/instance/state.js 源码路径
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

// src/core/observer/index.js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

如果传入的 props 是基本数据类型,子组件修改父组件传的 props 会警告,并且修改不成功,如果传入的是引用数据类型,那么修改改引用数据类型的某个属性值时,对应的 props 也会修改,并且 vue 不会抱警告。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!