Code Monkey home page Code Monkey logo

blog1's People

Contributors

plane-hjh avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

blog1's Issues

剑指offer-javascript版-用两个栈实现队列

题目描述

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]

示例 2:

输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

解法一

使用两个栈来模拟队列的操作。一个栈 inStack 模拟入栈操作,另一个栈 outStack 模拟出栈操作。首先插入一个元素 a,先入栈 inStack,此时 inStack 中的元素有 {a},而 outStack 为空。
再压入两个元素 bc 。还是入栈 inStack ,此时inStack 中的元素有 {a,b,c},而 outStack 还是为空。

当我们试着从队列中删除一个元素时,按照队列 先入先出 的规则。由于 ab, c 先插入队列,那么应该最先删除的是 a。但是由于 a 是存在栈 inStack 的栈底中,不能直接删除 a。但是我们如果把 inStack 的元素逐个弹出压栈到 outStack,这时候栈 outStack 中的元素为 {c,b,a}inStack 中的元素为空的。那么我们就可以直接弹出 outStack 的栈顶 a了。

/**
 * outStack 不为空:弹出元素
 * outStack 为空:将 inStack 元素依次弹出,放入到 outStack 中(在数据转移过程中,顺序已经从后入先出变成了先入先出)
 */
var CQueue = function() {
    this.inStack = []
    this.outStack = []
};

/**
 * @param {number} value
 * @return {void}
 */
CQueue.prototype.appendTail = function(value) {
    this.inStack.push(value)
};

/**
 * @return {number}
 */
CQueue.prototype.deleteHead = function() {
    const { inStack, outStack} = this

    // 如果要出的栈内没有元素,那么直接先把入栈内的元素,添加到出栈。然后再删除
    if(!outStack.length) {
        while(inStack.length) {
            outStack.push(inStack.pop())
        }
    }

    return outStack.pop() || -1
};

扩展

用两个队列实现一个栈

有两个队列 queue1queue2。当往栈内压入一个元素的时候,那么我们往队列 queue1 插入 a。接下来继续往栈内压入 bc 两个元素。也往队列 queue1 插入 bc。此时队列 queue1 的元素为 [a, b, c]

当我们要删除栈内的元素时,必须遵循栈的 先进后出。所以应该弹出的是 c。但是我们使用的是队列来实现删除操作,先进先出 这样的话只能弹出的是 a了。要注意的是 queue2 这时候还没元素。我们可以这样把 queue1 中的元素ab依次删除并插入到 queue1 中,之后再从队列 queue1 中删除 c。这样的话就相当于实现了从栈顶删除元素了。后续操作类似

image

路径总和

题目描述

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明: 叶子节点是指没有子节点的节点。

示例: 

给定如下二叉树,以及目标和 sum = 22

               5
              / \
             4   8
            /   / \
           11  13  4
          /  \      \
         7    2      1

返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2

解法一,深度优先遍历

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {boolean}
 */
var hasPathSum = function(root, sum) {
  // 根节点为空
  if (root === null) return false;

  // 只有一个根节点,主要是这里的两个判断
  if (root.left === null && root.right === null && root.val === sum) return true;

  // 把总和减去当前值递归下去
  const left = hasPathSum(root.left, sum - root.val);
  const right = hasPathSum(root.right, sum - root.val);
  return left || right;
};

解法二,迭代

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {boolean}
 */
var hasPathSum = function(root, sum) {
  if (root === null) return false;
  let stack = [root];
  let sumStack = [sum - root.val];
  while (stack.length > 0) {
    let node = stack.pop();
    let curSum = sumStack.pop();
    if (node.left === null && node.right === null && curSum === 0) {
      return true;
    }
    if (node.right !== null) {
      stack.push(node.right);
      sumStack.push(curSum - node.right.val);
    }
    if (node.left !== null) {
      stack.push(node.left);
      sumStack.push(curSum - node.left.val);
    }
  }
  return false;
};

这些都是二叉树遍历的延伸问题

leetcode-javascript版-链表中倒数第k个节点

题目描述

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

例:

给定一个链表: 1->2->3->4->5,  k = 2.

返回链表 4->5.

解法一,双指针

类似双指针

节点之间相距k个节点。即前面的节点到达k后,后面的节点才出发。当前面的节点到达链表的尾节点的时候,由于前面的节点和后面的节点永远相差 k 个节点,那么直接返回后面的节点即可。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */

 var getKthFromEnd = function(head, k) {
    if(!head || !k) return null

    let front = head
    let behind = head
    let index = 1

    while(front.next) {
        index++;

        front = front.next
        if(index > k) {
            behind = behind.next
        }
    }

    return (k <= index) && behind
};

单元测试

单元测试

测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在__test__文件夹中

测试

describe 块之中,提供测试用例的四个函数:before()after()beforeEach()afterEach()。它们会在指定时间执行(如果不需要可以不写)

describe('加法函数测试', () => {
  before(() => {// 在本区块的所有测试用例之前执行
  });

  after(() => {// 在本区块的所有测试用例之后执行
  });

  beforeEach(() => {// 在本区块的每个测试用例之前执行
  });

  afterEach(() => {// 在本区块的每个测试用例之后执行
  });


  it('1加1应该等于2', () => {
     expect(add(1, 1)).toBe(2);
  });
  it('2加2应该等于4', () => {
      expect(add(2, 2)).toBe(42);
  });
});

测试文件中应包括一个或多个 describe, 每个 describe 中可以有一个或多个 it ,每个 describe 中可以有一个或多个 expect.

describe 称为"测试套件"(test suite),it块称为"测试用例"(test case)

expect 就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误.

Enzyme

Enzyme 是一个用于 ReactJavaScript 测试实用程序,它使得更容易断言,操作和遍历您的 React 组件的输出,它模拟了 jQueryAPI,非常直观,易于使用和学习。

提供了三种测试方法

  • shallow

  • render

  • mount

shallow

shallow 在单元测试的过程中,浅渲染 将一个组件渲染成虚拟 DOM 对象,并不会渲染其内部的子组件,也不是真正完整的 React Render,无法与子组件互动。

mount

mount 方法用于将 React 组件加载为真实 DOM 节点。mount 会渲染当前组件以及所有子组件

render

render 采用的是第三方库 Cheerio 的渲染,渲染结果是普通的 html 结构,对于 snapshot 使用 render 比较合适。

但是,大多数情况下, shallow 方法就能满足我们的需求了

别的 API

get(index) 返回指定位置的子组件的DOM节点

at(index) 返回指定位置的子组件

first() 返回第一个子组件

last() 返回最后一个子组件

type() 返回当前组件的类型

test() 返回当前组件的文本内容

html() 返回当前组件的 HTML 代码形式

props() 返回根组件的指定特性

prop(key) 返回根组件的指定属性

state([key]) 返回根组件的状态

setState(nextState) 设置根组件的状态

setProps(nextProps) 设置根组件的属性

模拟 Props,渲染组件创建 Wrapper

/* eslint-env jest */
import { shallow } from 'enzyme';
import React from 'react';
import { OrderManage } from '../../components/purchaser/OrderManege';

const setup = ({ ...props }) => {
  const wrapper = shallow(<OrderManage {...props} />);
  return {
    props,
    wrapper,
  };
};
describe('OrderManage', () => {
  it('role is operator', () => {
    const { wrapper } = setup({
      role: 'operator',
      isFetching: true,
      fetchOrdersByStatuses: () => {}, // 直接设为空函数    getData: jest.fn(), // Jest 提供的mock 函数
    });
    const params = {
      node: {
        id: 2,
      },
    };
    expect(wrapper.instance().handlePageChange(1));
    expect(wrapper.instance().OrderManagementLink(params));
    expect(wrapper.find('.loader')).toHaveLength(1);
    expect(wrapper.find('.order-simpleGrid')).toHaveLength(0);
    expect(wrapper.type()).toEqual('div');
  });
});

在正式测试功能之前,我们要写一个 setup 方法用来渲染组件,因为每一个测试 case 都会用到它

模拟事件测试

<Input value={value} onChange={e => this.handleChange(e)}/>
it('can save value and cancel', () => {
   const value = 'edit'
   const {wrapper, props} = setup({
      editable: true
   });
   wrapper.find('input').simulate('change', {target: {value}});
   wrapper.setProps({status: 'save'});
   expect(props.onChange).toBeCalledWith(value);
})

可以在这个返回的 dom 对象上调用类似 jqueryapi 进行一些查找操作,还可以调用 setPropssetState 来设置 propsstate,也可以用 simulate 来模拟事件

触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。

wrapper.find('button').simulate('click')

wrapper.find('input').simulate('keyup')

expect(props.onClick).toBeCalled()// onClick方法被调用

expect(props.onClick).not.toBeCalled() // onClick方法没被调用

生命周期的测试

对于

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUnmount

可以使用 Enzyme 中的 shallow 方法加载组件

 /* eslint-env jest */
import { shallow } from 'enzyme';
import sinon from 'sinon';
import { App } from '../App';


it('componentWillMount', () => {
  sinon.spy(App.prototype, 'componentWillMount');
  shallow(<App />);
  expect(App.prototype.componentWillMount.calledOnce).toBeTruthy();
});
it('componentWillReceiveProps', () => {
  let wrapper = shallow(<App role={''} />);
  sinon.spy(App.prototype, 'componentWillReceiveProps')
  wrapper.setProps({
    role: 'admin'
  });
 expect(App.prototype.componentWillReceiveProps.calledOnce).toBeTruthy();
})

其中,spysinon 提供的特殊函数,它可以获取关于函数调用的信息。例如,调用函数的次数、每次调用的参数、返回的值、抛出的错误等,可以用来测试一个函数是否被正确地调用。npm i --dave-dev sinon 安装 sinon.

而对于

  • componentDidMount
  • componentDidUpdate

要用 enzymemount 方法进行加载。

使用snapshot进行UI测试

import renderer from 'react-test-renderer'

it('App -- snapshot', () => {
   const renderedValue = renderer.create(<App />).toJSON()
   expect(renderedValue).toMatchSnapshot()
})

jest 的特色,快照测试第一次运行的时候会将 React 组件在不同情况下的渲染结果(挂载前)保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较,diff 出两次快照的变化。

如果需要更新快照文件,使用 npm run test -- -u 命令

组件中的方法测试

export class Card extends React.Component {
  constructor (props) {
    super(props)

    this.cardType = 'initCard'
  }

  changeCardType (cardType) {
    this.cardType = cardType
  }
  ...
}
it('changeCardType', () => {
  let component = shallow(<Card />)
  expect(component.instance().cardType).toBe('initCard')
  component.instance().changeCardType('testCard')
  expect(component.instance().cardType).toBe('testCard')
})

其中,instance 方法可以用于获取组件的内部成员对象。

剑指offer-javascript版-找出数组中的数字

对应LeetCode上的题目:面试题03. 数组中重复的数字

题目描述:在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字

解法一,暴力求解法

先把输入的数字排序,从排序的数字中找出重复的数字,即需要从头到尾扫描排序后的数组就可以了,排序一个长度为 n 的数组需要 O(nlogn) 的时间(排序最快的速度是 O(nlogn) ,即快速排序)

  const findRepeatNumber = (nums) => {
    // 先排序
    nums.sort((a, b) => a - b)

    for(let i = 0, len = nums.length; i < len-1; i++) {
      // 比较前后的值是否相同
      if(nums[i] === nums[i+1]) {
        return nums[i]
      }
    }
  }

可想而知,这种解法并不是那么令人满意,因为需要先把数组排序

解法二,使用对象

使用对象来解决这个问题,从头到尾遍历整个数组,依次把数组的每一项当作对象的 key 值存进对象。如果数组的某个项在对象中存在,那么我们就可以认为这个值在数组中重复了,直接返回即可

const findRepeatNumber = (nums) => {
  // 用来存放是否遍历过的数组的项
  const obj = {}
  for(let i = 0; i < nums.length; i++) {
    // 数组的项是否已经是对象obj的键值了
    if(obj[nums[i]]) {
      return nums[i]
    } else {
      obj[nums[i]] = 1
    }
  }
}

解法三,数学分析法

从头到尾遍历数组

  • 当数组下标为 i 的时候,比较这个数字(用 m 表示)是不是等于 i,如果是,则接着扫描下一个数字。

  • 如果不是,拿它与第 m 个数字进行比较。如果它和第 m 个数字相等,就找到了一个重复的数组(im 的位置上都存在数字 m)。如果它和第 m 个数字不相等,就把第 i 个位置上的数字和第 m 个位置上的数字交换,那么 第 m 位上的数字就是 m

  • 重复这个比较,直到发现一个重复的数字

[2, 3, 1, 0, 2, 5, 3] 举个例子:

  • 首先遍历这个数组,从 0 位置上开始遍历。0 上面的数字是 2,那么我们可以直接拿数字 22 位置上的 1 做比较,结果不相等,交换位置。交换之后的数组是 [1, 3, 2, 0, 2, 5, 3]

  • 此时 0 位置上面的数字是 1,仍然不相等,继续和下标为 1 的 数字 3 做交换。交换之后的数组是 [3, 1, 2, 0, 2, 5, 3]

  • 此时 0 位置上面的数字是 3,仍然不相等,继续和下标为 3 的 数字 0 做交换。交换之后的数组是 [0, 1, 2, 3, 2, 5, 3]

  • 这时候 0 位置上的数字是 0 了,那么我们可以直接遍历下一个数字了,可知接下来的数字 123 都与坐标相等,不用进行交换操作。

  • 接下来到位置 4 的数字 2 ,由于数字与位置不相等,再比较位置 2 上的数字。发现位置 2 上的数字也是 2 了,因此我们便找到了一个重复的数字

const findRepeatNumber = (nums) => {
  for(let i = 0; i < nums.length; i++) {
    while(nums[i] !== i) {
      if(nums[i] === nums[nums[i]]) {
        return nums[i]
      }

      // 交换数组两个位置上的项,这里是i 和 num[i] 交换
      let temp= nums[i]
      nums[i] = nums[temp]
      nums[temp] = temp
    }
  }
}

剑指offer-javascript版-剪绳子

题目描述:给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]k[1]...*k[m] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

动态规划

如果面试题是求一个问题的最优解(通常是求最大值或者最小值),而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,就可以考虑使用动态规划来解决问题。

应用动态规划之前要分析能否把大问题分解成小问题,分解后的每个小问题也存在最优解。如果把小问题的最优解组合起来能够得到整个问题的最优解,那么我们可以应用动态规划解决这个问题

贪婪算法

贪婪算法和动态规划不一样,当我们应用贪婪算法解决问题的时候,每一步都可以做出一个贪婪的选择,基于这个选择,我们确定能够得到最优解

解法一,动态规划

  • 首先定义函数f(n) 为把长度为n的绳子剪成若干段后各段长度乘积的最大值

  • 在剪第一刀的时候,我们有 n-1 种可能的选择,也就是剪出来的第一段绳子的可能长度为 1,2,...,n-1。因此 f(n)=max(f(i)*f(n-i)),其中 0<i<n

  • 特殊情况,当绳子长度为2时,只能剪成长度为1的两段,f(2)的乘积为1.当长度为3的时候,只能剪成长度为1和2的两段长度,是最大的乘积,所以f(3)的乘积为2

const cuttingRope = function(num) {
    if(num < 2) return 0
    if(num === 2) return 1
    if(num === 3) return 2

    const products = []
    products[0] = 0
    products[1] = 1
    products[2] = 2
    products[3] = 3

    let max = 0
    for(let i = 4; i <= num; ++i) {
        max = 0;

        // 循环当前可以切段的所有结果,遍历一一比较求最大乘积
        for(let j = 1; j <= i/2; ++j) {
            let product = products[j] * products[i - j]

            if(max < product) {
                max = product;
                products[i] = max
            }
        }
    }

    max = products[num]

    return max
};

解法二,贪婪算法

观察规律,如果我们按照以下策略来剪绳子,则得到的各段绳子的长度的乘积将最大

  • 当n>=5时,我们尽可能多地剪长度为3的绳子

  • 当剩下的绳子为4时,把绳子剪成两段长度为2的绳子

  • 当剩下的绳子为2时,那么绳子就不用剪了

const cuttingRope = function(num) {
    if (n === 2) return 1;
    if (n === 3) return 2;

    const a = Math.floor(n / 3);

    const b = n % 3;

    // n是3的倍数
    // 绳子刚好是3的倍数
    if (b === 0) return Math.pow(3, a);

    // 当剩下的绳子为4时
    if (b === 1) return Math.pow(3, a - 1) * 4;

    // 当剩下的绳子为2时
    return Math.pow(3, a) * 2;
};

总结

能够灵活运用动态规划解决问题的关键是具备从上而下的分析能力,从下到上解决问题的能力。而灵活运用贪婪算法则需要扎实的数学基础功。

面试总结

面试总结

金证科技

  1. css3的了解
  • transition 过渡可以为一个元素在不同状态之间切换的时候定义不同的过渡效果
  • tranform 允许你旋转,缩放,倾斜或平移给定元素,
  • animation 用来指定一组或多组动画,每组之间用逗号相隔。
  1. 左边固定,右边自适应

html 代碼

<div class="parent">
    <div class="left"></div>
    <div class="right"></div>
</div>
  • 左边div使用float:left;
<style>
    .left {
        float: left;
        width: 500px;
        height: 200px;
        background: red;
    }

    .right {
        height: 200px;
        background: #FF6633;
        overflow: auto;
    }
</style>
  • 将外部容器div设置display:flex,flex-direction: row,将项目水平显示,正如一个行一样。并且将右边div设置flex:1,自适应宽度
.parent {
    display: flex;
}
.left {
    width: 200px;   /** 或者使用flex-basis: 200px; **/
    height: 200px;
    background: red;
}
.right {
    flex: 1;
    height: 200px;
    background: blue;
}
  • 设置左边容器的宽度,和float值,然后设置右边容器的margin-left为左边容器的宽度
.left{
    float: left;
    background: red;
    width: 500px;
    height: 500px;
}
.right{
    background: #FF6633;
    margin-left: 500px;
    height: 200px;
}
  • 让左边容器脱离文档流,设置右边容器的margin-left为左边容器的宽度
.parent{
    position: relative;
}
.left {
    position: absolute;
    background: red;
    width: 500px;
    height: 200px;
    left: 0;
    top: 0;
}
.right {
    margin-left: 500px;
    background: #FF6633;
    height: 200px;
}
  1. 怎么旋转5个圆圈
  2. 缓存
  3. 对webpack的了解
  4. 对vue的了解
  5. vue的组件通信
  • 父组件通过props向下传递数据给子组件

  • 子组件向父组件传值(通过事件形式)。子组件通过events给父组件发送消息,实际上就是子组件把自己的数据发送到父组件

  • 通过一个空的Vue实例作为**事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex

var Event=new Vue();
Event.$emit(事件名,数据);
Event.$on(事件名,data => {});
  1. 兄弟组件通信

eventBus 和 vuex

  1. vuex
  • Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上

  • 在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。

  • $attrs/$listeners

  • provide/inject

总结:

  • 父子通信:父向子传递数据是通过 props,子向父是通过 events($emit);通过父链 / 子链也可以通信($parent / $children);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners

  • 兄弟通信: Bus;Vuex

  • 跨级通信: Bus;Vuex;provide / inject API、$attrs/$listeners

  1. eventBus
  2. vuex和eventbus的优缺点
  3. 快排
function quickSort(arr) {
    if(arr.length <= 1) return arr
    
    let cur = arr.splice(0, 1)
    let leftArr = [];
    let rightArr = [];
    
    for(let i = 0, len = arr.length; i < len; i++) {
        if(arr[i] < cur) {
            leftArr.push(arr[i])
        } else {
            rightArr.push(arr[i])
        }
    }
    
    return quickSort(leftArr).concat(cur, quickSort(rightArr))
}

面试体验:7分,面试官使劲的问我vue,不过也有可能公司使用的是vue,之后我项目里也有写用了vue吧,怎么说呢,只能说自己没看清楚jd,技术栈不一样

TME-全民K歌

  1. chunk,module以及bundle的区别
  • chunk 多模块合并成的,如entry, import(), splitChunk

  • module 各个源码文件,webpack中一切皆模块

  • bundle 最新输出的文件

  1. 懒加载被打包成了什么

  2. webpack-dev-server 原理

参考:https://zhuanlan.zhihu.com/p/30669007

  1. react hooks 和 class 组件的区别
  2. react hooks 的useCallback
  • useCallback 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用
  1. react fiber

fiber 是一种基于浏览器的单线程调度算法,在react16之前,reconcilation 算法上实际上是递归,想要中断递归是很困难的,react16开始使用循环来代替之前的递归,是一种将recolication(递归diff)拆分,它随时能够停止,恢复。停止恢复的时机取决于当前的一帧内有没有足够的时机允许计算

  1. redux流程
  • 首先,用户通过view发出action,发出方式就用到了dispatch
  • 然后store自动调用reducer,并且传入两个参数:当前的state和收到的action,reducer会返回新的state
  • state一旦有变化,store就会调用监听函数来更新view
  1. 不可变数据 immutable.js
  • mutable实现的原理是Persistent Data Structur(持久化数据结构),对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象, 同时使用旧数据创建新数据时,要保证旧数据同时可用且不变。

  • 避免像 deepCopy一样 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

  1. 页面出现了性能问题,是怎样定位的

  2. vue3.0改进,vue改进了啥应对react

vue最主要的特点就是响应式机制、模板、以及对象式的组件声明语法,而3.0对这三部分都做了更改

  • 响应式的变化Object.defineProperty实现的代理,兼容主流浏览器和ie9以上的ie浏览器,能够监听数据对象的变化,但是监听不到对象属性的增删、数组元素和长度的变化,同时会在vue初始化的时候把所有的Observer都建立好,才能观察到数据对象属性的变化。3.0进行了革命性的变更,采用了ES2015的Proxy来代替Object.defineProperty,可以做到监听对象属性的增删和数组元素和长度的修改,还可以监听Map、Set、WeakSet、WeakMap,同时还实现了惰性的监听,不会在初始化的时候创建所有的Observer,而是会在用到的时候才去监听。

  • 模板 模板方面没有大的变更,只改了作用域插槽,2.x的机制导致作用域插槽变了,父组件会重新渲染,而3.0把作用于插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

  • 对象式的组件声明方式vue2.x中的组件是通过声明的方式传入一系列option,和TypeScript的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
    3.0修改了组件的声明方式,改成了类式的写法,这样使得和TypeScript的结合变得很容易。

  1. http1.0 http1.1以及http2的演进
  • http1.0 增加了很多命令push,post之类的。增加了status code 和header等相关内容,多字符集支持,多部分发送,权限缓存等

  • http1.1 支持持久连接,增加了pipeline,增加了host和其他一些命令

  • http2.0 二进制分帧 ,多路复用,服务器推送,头部压缩

  1. http2是如何多路复用的,如何头部压缩的。
  • 多路复用:Headers + Body的报文格式被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

  • 头部压缩:使用压缩算法HPACK,首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,...)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。

  1. http请求,只是cookie不一样,那么http2是如何复用的。

  2. http 和 https的区别

多了一个安全套层,即TSL/SSL。主要是用来加密。之前的是明文传输的,现在密文传输了

  1. https的传输流程
  • 客户端请求服务器获取证书公钥
  • 客户端(SSL/TSL)解析证书(无效会弹出警告)
  • 客户端生成随机值
  • 用公钥加密随机值生成秘钥
  • 客户端将秘钥发送给服务端
  • 服务端用私钥解密秘钥得到随机值,之后用随机值加密要加密的内容发送给客户端
  1. 抓包工具怎么抓取https,为什么

主要是要安装证书,拦截请求,伪造证书,返回证书

  1. 证书的内容有啥

电子签证机关的信息、公钥用户信息、公钥、权威机构的签字和有效期等等

  1. event loop, 点击事件怎么执行

执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

  1. es6中的Map和Set的区别。Map和对象的区别。Set和数组的区别
  • Oject:键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制

  • Map:似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

  • Set:类似于数组,但是成员的值都是唯一的,没有重复的值。Set 内部判断两个值是否不同
  1. 大文件上传

面试体验:9分。面试官很年轻,问的问题不会照着面经问,有深度有广度。也会给我一些提示和一些问题的答案,就是会一步一步引导你,给了一些学习的建议。会根据我的擅长来提问,答得大概有80%,开放性问题答得差,再接再厉啦。怎么说呢,也是对自己这两年来的学习做一个检测,一直在埋头前进,没有面过大厂,不知道前面的方向是啥,只有多面面才能进步。总之来说深度和广度还是不够呀。面试官说我这面过了的话,后面都是服务端的大佬问你的,所以服务端的知识缺乏,深度不够

明源云

一面

  1. css水平居中,垂直居中

答:首先最简单的就是flex布局,设置align-item和justify-content为center就可以了。
不用flex的时候,未知宽高的时候,可以设置绝对定位absolute和left、right、top、bottom为0就可以了。
已知宽高可以设置margin反向偏移+绝对定位absolute。或者绝对定位absolute(top:50%, left: 50%)和transform: tranlate(-50%, -50%)

  1. css权重

答:!important>行内样式>ID选择器 > 类选择器 | 属性选择器 | 伪类选择器 > 元素选择器

  • ID选择器, 权重:100;
  • class,属性选择器和伪类选择器,权重:10;
  • 标签选择器和伪元素选择器,权重:1;
  1. css命名方式

答:BEM

  • Bem 是块(block)、元素(element)、修饰符(modifier)的简写
  • -中划线 :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
  • __ 双下划线:双下划线用来连接块和块的子元素
  • _ 单下划线:单下划线用来描述一个块或者块的子元素的一种状态
  1. 移动端布局rem的原理

rem 是相对根元素html的font-size来参考的,假如说font-size的大小为100px,那么1rem的话就是100px

  • 将布局适口设置为视觉视口,不进行缩放,即理想视口。<meta name="viewport"content="initial-scale=1,maximum-scale=1, minimum-scale=1”>

  • 以设计稿分辨率为基准,取100px为font-size的参照,那么设计稿的宽如果是640,body元素的宽度就可以设置为width:6.4rem(640/100),当我们将布局视口设置为320时,于是html的font-size=deviceWidth / 6.4。

  • 通过document.documentElement.clientWidth获取deviceWidth;

  • 当页面的dom ready后设置html font-size(document.documentElement.style.fontSize =document.documentElement.clientWidth / 6.4 + ‘px’)

  • 通过mediaQuery 设置字体大小,字体大小不可以使用rem,原因是误差太大

例:

// 以640的设计稿为例最终的设置html font-size代码如下,布局时拿设计稿标注的尺寸除以100,就是rem的值,相当简单啊

var deviceWidth = document.documentElement.clientWidth;
if(deviceWidth > 640) deviceWidth = 640;
document.documentElement.style.fontSize = deviceWidth / 6.4 + 'px';
  1. 移动端1px的问题,出现的原因?

需要理解三个概念

  • 物理像素:一个物理像素是显示器(手机屏幕)上最小的物理显示单元,可以理解为我们平时说的分辨率

  • 设备独立像素:设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素,在这里可以理解为我们说的视觉视口的大小;所以说,物理像素和设备独立像素之间存在着一定的对应关系,这就是接下来要说的设备像素比。

  • 设备像素比:设备像素比(简称dpr)定义了物理像素和设备独立像素的对应关系,它的值可以按如下的公式的得到:
    设备像素比 = 物理像素 / 设备独立像素 // 在某一方向上,x方向或者y方向。
    设备像素比也是设备生产的时候设置好的,在javascript中,可以通window.devicePixelRatio获取到当前设备的dpr。

  1. css问题,设计一个dialog,当只有一行文字的时候,水平居中。两行文字的时候向左居中
  2. js的sessionStorage,localStorage,cookie的区别

必会的,不做解释

  1. cors怎么设置
  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求

  • Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,开发者必须在AJAX请求中打开withCredentials属性

  1. 怎么获取服务器上面的时间(传输的时候需要耗费时间)
  2. 假如有个抢单的按钮,前端怎么处理点击多次,发送请求的问题

设置一个变量用来判断有没有发起请求,如果发起请求了,就设置false,按钮不能点击了。请求回来了设置true,可以点击

  1. webpack的大小优化,构建流程优化

大小优化:

  • 生产环境production,
  • 使用webpack-bundle-analyzer或者speed-measure-webpack-plugin 分析文件打包的体积,
  • antd按需加载
  • 使用高版本的webpack和node,
  • 压缩js和css代码,
  • tree-shaking 删除没引用到的代码,
  • mini-css-extract-plugin提取css,
  • DllPlugin分包

构建优化:

  • 高版本的webpack和node

  • 多进程/多实例构建(happypack, thread-loader)

  • 压缩代码,多进程并行压缩(webpack-paralle-uglify-plugin,terser-webpack-plugin)

  • 缩小打包作用域

  • 提取页面公共资源

  • dll

  • 利用缓存提升二次构建速度(babel-loader)

  1. setState是同步还是异步的
  • setState的话在原生事件和setTimeout里面是同步的
  • 在钩子函数和react的合成事件中是异步的

本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法拿到更新后的值,形成了所谓的异步。

在原生事件和setTimeout 中不会批量更新,在异步中如果对同一个值进行多次setState,setState的批量更新策略会对其覆盖,取最后一次执行。如果是同时setState多个不同的值,在更新时会对其进行合并批量更新

  1. 在react代码中怎么注重性能优化

主要是SCU,pureComponent,memo,this的绑定问题,不可变数据

  1. 原公司业务流程

  2. 前后端联调是怎么样的,除了mock有没有更好的方案?

  3. 原型和原型链

  4. find 和 filter的区别

find的话找到符合条件的话就返回,filter是个过滤器,会遍历元素的所有节点,再返回满足条件的元素

  1. react和之前的jquery的区别在哪,为什么大家都使用react

  2. 怎么样才能链式调用

  3. let,const和var之间的区别

二面

  1. 用webpack做了哪些优化?
  2. 原公司的业务以及开发流程
  3. 最近有关注哪些前端新动向
  4. http和https的区别
  5. tsl/ssl位于七层协议的哪一层

面试体验:一般,面试官挺好的,一面是基础中的基础了,二面的话主要是了解你做过的一些项目,没有问技术上面的问题。我觉得主要还是在两个原因,一是在一面中一些开放性的问题答得不好,平常自己碰到的场景不多。二是因为自己的年限问题吧。被评为了初级,给不了期望薪资,那只能抱歉了

平衡二叉树

题目描述

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

   3
   / \
  9  20
     / \
   15   7

返回 true

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3  3
  / \
 4   4

返回 true

解法一,后序遍历

  • 比较左右子树的深度,若差值大于1 则返回一个标记 -1表示当前子树不平衡

  • 左右子树有一个不是平衡的,或左右子树差值大于1,则整课树不平衡

  • 若左右子树平衡,返回当前树的深度(左右子树的深度最大值+1)

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function(root) {
    function balanced(node) {
      if (!node) {
        return 0;
      }
      const left = balanced(node.left);
      const right = balanced(node.right);
      if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
        return -1;
      }
      return Math.max(left, right) + 1;
    }

    return balanced(root) !== -1
};

人类的大脑是如何学习到知识的?

通过不断的思考,在不断地学习和思考的过程中,人的大脑内部产生着剧烈的神经活动,迫使脑神经之间建立了更多的连接。随着神经元之间连接越来越多。不仅如此,甚至还能触类旁通,举一反三,乃至和其他的领域结合,产出创新性的想法,这表现出来,就是你更聪明了。

当有了一台服务器之后我做了什么

服务器相关

摘录于当我有一台服务器时我做了什么

登录

使用ssh登录

ssh root@182.92.237.90

但是这样子登录的话,需要记住 ip 地址,所以可以通过配置 ssh-config 达到不用输入 ip 地址的目的。关于 ssh-config 的配置文件

  • /etc/ssh/ssh_config
  • ~/.ssh/config
# 修改 ssh 配置文件 plane 是要使用的登录名

Host plane
	HostName 182.92.237.90
	User root

配置完成之后,我们就可以使用以下方式登录了

ssh plane

免密登录

虽然可以通过用户名登录,但是还是需要输入密码,下面我们实现免密登录

  1. 两个文件: 本地环境的 ~/.ssh/id_rsa.pub 与 远程服务器的 ~/.ssh/authorized_keys
  2. 一个动作:把本地文件中的内容复制粘贴到远程服务器中

总结成一句话,即把自己的公钥放在远程服务器。

直接把 id_rsa.pub 的内容复制到 远程服务器上的 authorized_keys 就可以了

环境配置

git 安装和配置

安装

yum install git

git 基本配置

git config --global user.name plane
git config --global user.email 18211495117@163.com

可以测试下是否可以连接 github

$ ssh -T git@github.com
git@github.com: Permission denied (publickey).

linux 基础

查看 centos 版本号

$ cat /etc/centos-release
CentOS Linux release 8.1.1911 (Core)

内存配额及使用情况,查看还有多少内存,available 指还有多少可用内存

-h 指打印可视化信息

$ free -h
total        used        free      shared  buff/cache   available
Mem:          3.6Gi       164Mi       2.9Gi       0.0Ki       483Mi       3.2Gi
Swap:            0B          0B          0B

CPU 核心数量及使用率

# 查看 cpu 的核心数
$ cat /proc/cpuinfo

# 查看
$ top

$ htop

磁盘使用情况

$ df -h
文件系统        容量  已用  可用 已用% 挂载点
devtmpfs        1.8G     0  1.8G    0% /dev
tmpfs           1.8G     0  1.8G    0% /dev/shm
tmpfs           1.8G  464K  1.8G    1% /run
tmpfs           1.8G     0  1.8G    0% /sys/fs/cgroup
/dev/vda1        40G  2.7G   38G    7% /
tmpfs           364M     0  364M    0% /run/user/0

平均负载

load average 指单位时间内运行态进程及不可中断进程的平均进程数,运行态进程指正在使用或者等待使用 CPU 的进程,不可中断进程指正等待一些 IO 操作的进程。可使用 uptime 查看此指标。

$ uptime
23:28:05 up 42 min,  2 users,  load average: 0.00, 0.00, 0.00

登录用户

$ who -u
root     pts/0        2020-08-29 22:46  旧�       1220 (119.123.224.176)
root     pts/1        2020-08-29 23:09  旧�       1316 (119.123.224.176)

docker 环境配置

先安装依赖

yum install -y yum-utils device-mapper-persistent-data lvm2

添加 docker 的yum镜像源,如果在国内,添加阿里云的镜像源

# 安装 docker 官方的镜像源
$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 如果在国内,安装阿里云的镜像
$ yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装指定版本的 docker 并且启动服务

# 安装 docker
$ yum install -y docker-ce

# 安装指定版本号的 docker,以下是 k8s 官方推荐的 docker 版本号 (此时,k8s 的版本号在 v1.16)
$ yum install -y docker-ce-18.06.2.ce

$ systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.

$ systemctl start docker

安装 docker-ce 时候如果有报错,

错误:
 问题: package docker-ce-3:19.03.12-3.el7.x86_64 requires containerd.io >= 1.2.2-3, but none of the providers can be installed

则需要安装 containerd

yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/containerd.io-1.2.6-3.3.fc30.x86_64.rpm

docker 安装成功后,我们可以使用以下命令查看版本号

$ docker --version
Docker version 18.06.2-ce, build 6d37f41

# 查看更详细的版本号信息
$ docker version

# 查看docker的详细配置信息
$ docker info

Docker Compose 安装

sudo curl -L "https://get.daocloud.io/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

之后使用

chmod +x /usr/local/bin/docker-compose

验证安装:

docker-compose version

浏览器缓存的基本认识

之前一直对浏览器缓存只能描述一个大概,深层次的原理不能描述上来;为了泄恨,查阅一些资料最终对其有了一个更深入的理解

本文主要讲解浏览器端的缓存,缓存的作用是不言而喻的,能够极大的改善网页性能,提高用户体验。

浏览器缓存的基本认识

分为强缓存协商缓存

(1)浏览器在加载资源的时候,会根据HTTP的头部header判断它是否命中强缓存,强缓存如果直接命中,那么浏览器就会直接从自己的缓存中读取资源,而不会请求到服务器。比如某个css文件,如果浏览器在加载这个文件的时候,从这个文件的http头部判断命中了缓存,那么就会直接从内存中读取这个css文件,而不会再去请求服务器。

(2)当强缓存没有命中的时候,那么浏览器就会发送http请求到服务器,通过服务器依据资源的另外一些http头部去验证这个资源是否命中协商缓存,如果协商缓存命中,服务器就将这个请求返回,但是并不会返回这个资源的数据,而是告诉浏览器可以直接从缓存中读取这个资源,那么浏览器就会从缓存中读取这个资源

(3)强缓存和协商缓存的共同点:如果命中,那么都会从客户端的缓存中去读取资源,而不是从服务器中读取资源。不同的是,强缓存不会发送请求到服务器,而协商缓存则需要发送请求到服务器。

(4)如果协商缓存也没有命中的时候,浏览器直接从服务器中加载资源数据。

强缓存

当浏览器对某个资源的请求命中了强缓存时,返回的 http 状态为200,在 chrome 的开发者工具的 network 里面 size 会显示为 from cache

强缓存是利用 Expires 或者 Cache-Control 这两个 http response header 实现的,它们都用来表示资源在客户端缓存的有效期。

一,Expires

Expireshttp1.0 提出的一个表示资源过期时间的 header,它描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的缓存原理是:

(1)浏览器第一次跟服务器请求一个资源的时候,服务器在返回这个资源的同时,会在 response 的 header 加上 Expiresheader,如
cacheControl

一,Expires

(2)浏览器在接收到这个资源后,会把这个资源连同所有的 response header 一起缓存下来(所有缓存命中的请求返回的 header 并不是来自服务器,而是来自之前缓存的 header

(3)浏览器再请求这个资源的时候,会先从缓存中去寻找,最好找到这个资源后,会拿当前的时间和Expire设置的时间比较,如果当前请求的时间在 Expires 指定的时间之前,那么就能命中缓存,否则不命中。

(4)如果缓存没有命中,那么浏览器直接从服务器中加载资源时,Expire Header 在重新加载的时候就会被更新。

弊端:Expires是较老的强缓存管理header,由于它是服务器返回的一个绝对时间,在服务器时间与客户端时间相差较大时,缓存管理容易出现问题,比如随意修改下客户端时间,就能影响缓存命中的结果。

所以在 HTTP1.1 的时候,就提出了一个新的header:Cache-Control,这是一个相对的时间,在配置缓存的时候以秒为单位,用数值表示,如:Cache-Control:max-age=315360000

二,Cache-Control

(1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在 responeheader 加上 Cache-Controlheader,如:

二,Cache-Control

(2)浏览器在接收到这个资源后,会把这个资源连同所有 response header 一起缓存下来;

(3)浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,根据它第一次的请求时间和 Cache-Control 设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行。(4)如果缓存没有命中,浏览器直接从服务器加载资源时,Cache-Control Header 在重新加载的时候会被更新。

Cache-Control 描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较Expires,Cache-Control的缓存管理更有效,安全一些。这两个header可以只启用一个,也可以同时启用,当response header 中,ExpiresCache-Control 同时存在时,Cache-Control 优先级高于Expires

协商缓存

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串,比如你打开京东的首页,按f12打开开发者工具,再按f5刷新页面,查看 network,可以看到有不少请求就是命中了协商缓存的:

协商缓存

协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的

一,【Last-Modified,If-Modified-Since】

(1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上 Last-Modified 的header,这个header表示这个资源在服务器上的最后修改时间:

Last-Modified

(2)浏览器再次跟服务器请求这个资源时,在request的header上加上 If-Modified-Since 的header,这个header的值就是上一次请求时返回的Last-Modified的值:

If-Modified-Since

(3)服务器再次收到资源请求时,根据浏览器传过来 If-Modified-Since 和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加 Last-Modified的header,因为既然资源没有变化,那么 Last-Modified 也就不会改变,这是服务器返回304时的 `response header :

header

(4)浏览器收到304的响应后,就会从缓存中加载资源。
(5)如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值。

弊端:【Last-Modified,If-Modified-Since】都是根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合起来管理协商缓存是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。

所以就有了另外一对header来管理协商缓存,这对header就是【ETag、If-None-Match】。

二,【ETag、If-None-Match】

(1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系,所以能很好的补充Last-Modified的问题:

etag

(2)浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值

ifNoMatch

(3)服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化:

etag2

(4)浏览器收到304的响应后,就会从缓存中加载资源。

三数之和

题目描述

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

例:

给定数组 nums = [-1, 0, 1, 2, -1, -4]

满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

解法一,暴力法

直接写三层嵌套的循环,把循环的项加起来,判断是否等于0即可。但是这种做法的时间复杂度为 O(n^3),不是我们需要追求的解法。

var threeSum = function(nums) {
    let res = []
    for (let i = 0; i < nums.length - 2; i++) {
        for (let j = i + 1; j < nums.length - 1; j++) {
            for (let k = j + 1; k < nums.length; k++) {
                if (nums[i] + nums[j] + nums[k] === 0) {
                    res.push([nums[i], nums[j], nums[k]])
                }
            }
        }
    }
    return res
}

解法二,c=-(a+b)

这个解法类似于 两数之和 的解法。直接用 哈希表 结构来存储已经访问过的元素,双层循环判断 -(a+b) 是否已经访问过了。所以时间复杂度为 O(n^2),空间复杂度为 O(n)

var threeSum = function(nums) {
        let res = []
        let hash = {}
        for (let i = 0; i < nums.length - 2; i++) {
            for (let j = i + 1; j < nums.length - 1; j++) {
                if (hash[nums[j]] !== undefined) {
                    res.push([nums[j]].concat(hash[nums[j]]))
                    hash[nums[j]] = undefined
                } else {
                    let mark = 0 - nums[i] - nums[j]
                    hash[mark] = [nums[i], nums[j]]
                }
            }
        }
        return res
}

解法三,先排序后查找

替换空格

题目描述

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

例:

输入:s = "We are happy."
输出:"We%20are%20happy."

限制:

0 <= s 的长度 <= 10000

解法一,正则表达式

replace

/**
 * @param {string} s
 * @return {string}
 */
var replaceSpace = function(s) {
    return s.replace(/\s /g, '%20')
};

解法二,splitjoin

var replaceSpace = function(s) {
    return s.split(/\s/).join('%20')    
};

面试题05. 替换空格

有效的括号

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

示例 1:

输入: "()"
输出: true

示例 2:

输入: "()[]{}"
输出: true

示例 3:

输入: "(]"
输出: false

示例 4:

输入: "([)]"
输出: false

示例 5:

输入: "{[]}"
输出: true

解法一,使用栈

/**
 * @param {string} s
 * @return {boolean}
 */
// 可以使用栈 
var isValid = function(s) {
    const map = {
        "(": ")",
        "[": "]",
        "{": "}"
    }

    let leftArr = []

    for(let ch of s) {
        if(map[ch]) {
            leftArr.push(ch);   // 为左括号时,顺序保存
        } else {
            // 右括号时,与数组末尾匹配
            if(ch != map[leftArr.pop()]) {
                return false
            }
        }
    }

    return !leftArr.length  // 防止全部为左括号或有多余的左括号
};

解法二,replace

找到最内层的括号对,消去,重复此过程,若存在无法消去的字符则说明字符串无效。

var isValid = function (s) {
    while (s.length) {
        var temp = s;

        // 把有效的括号抵消
        s = s.replace('()', '').replace('[]', '').replace('{}', '');

        // 判断抵消前和抵消后是否相等,相等则证明抵消不了有效括号,返回 false
        if (s == temp) return false
    }
    return true;
};

作者:rhinoc
链接:https://leetcode-cn.com/problems/valid-parentheses/solution/javascript-you-xiao-de-gua-hao-by-rhinoc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

有效的括号

剑指offer-javascript版-旋转数组的最小数字

对应LeetCode上的题目:面试题11. 旋转数组的最小数字

题目描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1

示例1:

输入:[3,4,5,1,2]
输出:1

示例2:

输入:[2,2,2,0,1]
输出:0

解法一,二分查找

这道题目最简单的做法就是直接遍历,找到数组的最小元素。但是这样并不是我们想要的答案,因为说好的旋转数组呢,一个旋转数组的特性都没有用上,那可不是面试官想要的答案。

  • 旋转之后的数字实际上可以分为两个排序的子数组,前面子数组的元素都大于或者等于后面子数组的元素。最小的数组刚好是这两个子数组的分界线

  • 二分查找法,两个指针,分别指向数组的第一个元素和最后一个元素

  • 找到数组中间的元素,如果该元素大于第一个指针指向的元素,那么最小值在中间元素的后面。如果小于第一个指针指向的元素,那么最小值在中间元素的前面,以此类推...

var minArray = function(numbers) {
  if(!numbers || numbers.length <= 0) return 0

  let left = 0
  let right = numbers.length - 1

  while(left < right) {
    let indexMid = Math.floor((left + right)/2)

    // 数组或者子数组有序的时候
    if(numbers[left] < numbers[right]) {
        return numbers[left]
    }

    // 左子数组有序,最小值在右边
    // 那么mid肯定不可能是最小值(因为numbers[mid]大于numbers[left])
    if(numbers[left] < numbers[indexMid]) {
      left = indexMid + 1
    } else if(numbers[indexMid] < numbers[right]) {
      // 右子数组有序,最小值在左边
      // 这里right=mid因为最小值可能就是numbers[mid]
      right = indexMid;
    } else {
        // 无法判断
        ++left
    }
  }

  return numbers[left]
};

leetcode-二维数组中的查找

题目描述

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

例:

给定 target = 5,返回 true。
给定 target = 20,返回 false。

解法一

取右上角的元素做比较

如果右上角的元素和整数相等,直接返回 true
如果右上角的元素小于整数,那么 ++row
如果右上角的元素大于整数,那么 --col

var findNumberIn2DArray = function(matrix, target) {
    if(!matrix) return false

    // 行
    let rows = matrix.length
    // 列
    let cols = matrix[0].length

    if(rows >= 0 && cols >= 0) {
        // 取右上角的值做比较
        let row = 0;
        let col = cols - 1;

        while(row < rows && col >= 0) {
            if(matrix[row][col] === target) {
                return true
            } else if(matrix[row][col] > target) {
                --col;
            } else {
                ++row;
            }
        }
        
    }

    return false
};

旋转矩阵

题目描述

给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。

不占用额外内存空间能否做到?

示例 1:

给定 matrix = 
[
  [1,2,3],
  [4,5,6],
  [7,8,9]
],

原地旋转输入矩阵,使其变为:

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

示例 2:

给定 matrix =

[
  [ 5, 1, 9,11],
  [ 2, 4, 8,10],
  [13, 3, 6, 7],
  [15,14,12,16]
]

原地旋转输入矩阵,使其变为:

[
  [15,13, 2, 5],
  [14, 3, 4, 1],
  [12, 6, 8, 9],
  [16, 7,10,11]
]

leetcode 地址:旋转矩阵

解法一:倒序倒置法

[
  [1,2,3],
  [4,5,6],
  [7,8,9]
],
  1. 将矩阵转置为
[
  [1,4,7],
  [2,5,8],
  [3,6,9]
],
  1. 将矩阵每一行倒序为
[
  [7,4,1],
  [8,5,2],
  [9,6,3]
]

实现代码:

const rotate = (matrix) =>{
    for(let i = 0; i < matrix.length; i++){
        for (let j = i; j < matrix[i].length; j++){
            [matrix[i][j],matrix[j][i]] = [matrix[j][i],matrix[i][j]]
        }
    }
    matrix.forEach(row=> row.reverse())
};

用最简洁代码实现 indexOf 方法

str.indexOf(searchValue [, fromIndex])

searchValue:要被查找的字符串值,默认 undefined

fromIndex: 可选值。数字表示开始查找的位置。可以是任意整数,默认值为 0。如果 fromIndex 的值小于 0,或者大于 str.length ,那么查找分别从 0 和str.length 开始。

参考出处:indexOf()

解法一,正则

function indexOf(str, a, start = 0) {
   if(start<0) start+=str.length;
   if(start>=str.length) return -1;

   const reg = new RegExp(`${a}`, 'gi')
   reg.lastIndex = start
   const result = reg.exec(str)
   return result ? result.index : -1
}

解法二,遍历

function indexOf(str, a, start = 0) {
    if(start<0) start+=str.length;
    if(start>=str.length) return -1;

    for (let i = start, len = str.length; i < len; i++) {
        if (str[i] == a) return i;
    }
    return -1;
}

两数之和

题目描述

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解法一,双层循环

使用双层循环,暴力求解就可以了。但是时间复杂度为 O(n^2)

var twoSum = function(nums, target) {
    for(let i = 0, len = nums.length; i< len; i++) {
        let index = nums.indexOf(target - nums[i])
        if(index > -1 && index !== i) {
            return [i, index]
        }
    }
};

解法二,使用 map

创建一个对象,只需要循环一次,拿 target - nums[i] 做比较,判断差值是不是对象的 key 就可以了。如果不是把当前值 nums[i] 作为对象的 key 值,下标作为对象 key 值的 value

var twoSum = function(nums, target) {
    let obj = {};
    for(let i = 0, len = nums.length; i< len; i++) {
        let num = target - nums[i]
        
        if(obj.hasOwnProperty(num)) {
            return [i, obj[num]]
        }
        obj[nums[i]] = i
    }
};

剑指offer-javascript版-斐波那契数列

题目描述:写一个函数,输入n,求斐波那契数列的第n项。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

解法一,递归

由题目描述可以知道,斐波那契数列的第一项是 0,第一项是 1 ,第 n 项是第 n-1 和第 n-2的和,那么我们就可以下手了

// 递归
const fib = (n) => {
    if(n === 0) return 0

    if(n === 1) return 1

    return fib(n - 1) + fib(n - 2)
}

解法二,递归+备忘录

使用递归来解这道题可以说是相当简单了,但是有时候面试官并不想看到我们单纯的使用递归去解这一道题,因为计算量太多重复的了。例如我们在计算 n=9 的时候。

f(9) = f(8) + f(7)
那么
f(8) = f(7) + f(6)

可以知道f(7) 被计算了两次,以此类推可以知道,我们在计算 f(9) 的时候,重复了太多了计算量了。那么我们可以使用 备忘录 的解法把已经计算过的值保存下来,下次如果再计算相同的值的时候,直接从备忘录里面取就可以了

// 在动态规划的一种做法中,可以借助“备忘录”来实现结果的缓存,避免重复计算
const fib = (n) => {
    // 备忘录
    const cache = {
        0: 0,
        1: 1
    };
    return Fibonacci(n);

    function Fibonacci(n) {
        if (cache[n] !== undefined) {
            return cache[n];
        }

        cache[n] = Fibonacci(n - 1) + Fibonacci(n - 2);
        return cache[n];
    }
};

解法三,循环+备忘录

我们也可以使用循环来替代递归

const fib = (n) => {
    // 备忘录
    const cache = {
        0: 0,
        1: 1
    };

    if(n < 2) {
        return cache[n]
    }

    let fibNone = 1
    let fibNtwo = 0
    let fibN = 0

    for(let i = 2; i <= n; i++) {
        fibN = fibNone + fibNtwo

        fibNtwo = fibNone
        fibNone = fibN
    }

    return fibN
};

扩展

类似题目:

  • 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

二叉树的层序遍历

题目描述

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

例如:
给定二叉树 [3,9,20,null,null,15,7] ,

    3
   / \
  9  20
    /  \
   15   7

返回其自底向上的层次遍历为:

[
  [15,7],
  [9,20],
  [3]
]

解法一:BFS(广度优先遍历)

BFS 是按层层推进的方式,遍历每一层的节点。题目要求的是返回每一层的节点值,所以这题用 BFS 来做非常合适。
BFS 需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列。

var levelOrderBottom = function(root) {
    if(!root) return []
    let res = [], 
        queue = [root]
    while(queue.length) {
        let curr = [],
            temp = []
        while(queue.length) {
            let node = queue.shift()
            curr.push(node.val)
            if(node.left) temp.push(node.left)
            if(node.right) temp.push(node.right)
        }
        res.push(curr)
        queue = temp
    }
    return res.reverse()
};

复杂度分析

时间复杂度:O(n)
空间复杂度:O(n)

解法二:DFS(深度优先遍历)

DFS 是沿着树的深度遍历树的节点,尽可能深地搜索树的分支

DFS 做本题的主要问题是: DFS 不是按照层次遍历的。为了让递归的过程中同一层的节点放到同一个列表中,在递归时要记录每个节点的深度 depth 。递归到新节点要把该节点放入 depth 对应列表的末尾。

当遍历到一个新的深度 depth ,而最终结果 res 中还没有创建 depth 对应的列表时,应该在 res 中新建一个列表用来保存该 depth 的所有节点。

var levelOrderBottom = function(root) {
    const res = []
    var dep = function (node, depth){
        if(!node) return
        res[depth] = res[depth]||[]
        res[depth].push(node.val)
        dep(node.left, depth + 1)
        dep(node.right, depth + 1)
    }
    dep(root, 0)
    return res.reverse()   
};

复杂度分析:

时间复杂度:O(n)
空间复杂度:O(h)h 为树的高度

解法三:递归

var levelOrderBottom = function(root) {
    const result = []
    dep(root, 0)
    function dep(root,num){
        if(!root) return
        result[num] = result[num]||[]
        result[num].push(root.val)
        dep(root.left, num+1)
        dep(root.right, num+1)
    }
    return result.reverse()
    
};

剑指offer-javascript版-从尾到头打印链表

对应LeetCode上的题目:面试题06. 从尾到头打印链表

题目描述:输入一个链表的头节点,从尾到头反过来打印出每个节点的值,链表节点定义如下:

function ListNode(val) {
    this.val = val;
    this.next = null;
}

解法一,使用栈

不改变链表的前提下。

遍历链表的顺序都是从头到尾,但是输出的顺序是从尾到头,也就是说第一个遍历的节点最后一个输出,最后一个遍历的节点第一个输出。这个不就是后进先出 吗?那么我们可以使用 来实现这种做法。

  • 每经过一个节点的时候,就把该节点放到一个栈中,当遍历完整个链表后。再从栈顶逐个输出节点的值
const printReverseListNode = (head) => {
    if (!head) return []

    // 栈用来存放已经遍历过的数组
    const stack = []
    let curNode = head

    while(curNode) {
        stack.push(curNode.val)

        curNode = curNode.next
    }

    return stack.reverse()
}

可见,使用栈来解决这类问题,是非常的简单。但是我们也要熟知栈的各种概念,我们才能在解题的时候游刃有余,一点都不慌。

当然啦,我们也可以使用递归来实现这种解法

const printReverseListNode = (head) => {
    if (!head) return []
    const stack =[]

    const getReverseListNode = (head) => {
        if(head) {
            if(head.next !== null) {
                printReverseListNode(head.next)
            }
            stack.push(head.val)
        }
    }

    getReverseListNode(head)
    return stack
}

leetcode-javascript版-反转链表

题目描述

反转一个单链表。

例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

解法一,迭代

主要是区别前后节点的替换

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
/**
 * 迭代
 */
var reverseList = function(head) {
    if (!head || !head.next) return head;

    let cur = head;
    let pre = null;

    while(head && head.next) {
        // 获取当前节点的下一个节点
        pre = head.next;

        // 使当前节点的下一个节点指向,下下个节点即:head.next = head.next.next
        head.next = pre.next;

        // 反转节点。使下一个节点的下个节点指向当前节点
        pre.next = cur;

        // 使下一个节点赋值为当前节点
        cur = pre;
    }

    return cur;
};

解法二,递归

/**
 * 递归
 */
var reverseList = function(head) {
  // 递归结束条件
  if (head === null || head.next === null) {
    return head
  }

  // 递归反转 子链表
  let newReverseList = reverseList(head.next)
  // 获取原来链表的第2个节点newReverseListTail
  let newReverseListTail = head.next
  // 调整原来头结点和第2个节点的指向
  newReverseListTail.next = head
  head.next = null

  // 将调整后的链表返回
  return newReverseList
}

javascript深入理解之this

简单轻松的理解this,就要记住一句话:this的指向并不是在函数定义的时候确定的,而是在其被调用的时候确定的。也就是说,this是在运行的时候基于函数的执行环境绑定的。

this的指向可以分为5大类:

  • 纯粹的函数调用
  • 作为对象的属性调用
  • 作为构造函数调用
  • apply,call调用
  • 箭头函数调用

一、 纯粹的函数调用

纯粹的函数调用,也就是最普遍的函数调用方式。非严格模式下,this指向的是window。严格模式下,this指向了undefined。

// 非严格模式下
var a = 10;    // 全局变量相当于在window上面挂载属性a,window.a
function func() {
    "use strict";
    console.log(this.a);
}

func();    // 10

// 严格模式下
var b = 10;
function func1() {
    "use strict";
    console.log(this.b);
}

func1();    // Uncaught TypeError: Cannot read property 'b' of undefined

二、作为对象的属性调用

函数作为对象的属性调用时候,this 指向这个对象。(实际上纯粹的函数调用也可以算是作为对象的函数调用,因为全局变量是直接挂载在window上的,所以 func() 也就相当于 window.func() )

var a = 20;
var obj = {
    a: 10,
    func: func
}

function func() {
    console.log(this.a);
}

obj.func()    // this指向了obj,所以this.a会打印出10

要注意的是,如果像下面这种情况,this 指向的是window的

var a = 20;
var obj = {
    a: 10,
    func: func
}

function func() {
    console.log(this.a);
}

var func1 = obj.func

func1()    // 20, this指向了window

所以说还得看函数调用的位置。

三、作为构造函数调用

通过构造函数,可以生成一个新对象(这个也就是我们经常说的实例)。这时,this就指这个新对象。

function Func() {
    this.a = 10;
}

var obj = new Func()
console.log(obj.a);    // 10

使用new调用构造函数实际上会经历四个过程:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象。(this指向了新对象)
  3. 执行构造函数中的代码。(挂载属性)
  4. 返回新的对象

如何手动实现new的功能?

function create() {
    // 创建一个新的对象
    var obj = new Object(),
    // 获得构造函数,arguments中去除第一个参数,Func指的是构造函数
    Func = [].shift.call(arguments);
    // 将构造函数的作用域赋给新对象。
    obj.__proto__ = Func.prototype;
    // 执行构造函数中的代码,使obj 可以访问到构造函数中的属性
    var res = Func.apply(obj, arguments);
    // 返回新的对象。判断构造函数的返回值是否是对象,如果是就返回。不是就返回新的对象。
    return res instanceof Object ? res : obj;
};

function Func() {
    this.a = 20;
}

// 使用new
var func1 = new Func()
                        
// 使用手写的new,即create
var func2 = create(Func)

console.log(func1, func2); // {a: 20}, {a: 20}

四、apply、call调用

apply、call都是是函数的一个方法,作用是改变函数的调用对象。它们的第一个参数就表示改变后的调用这个函数的对象。this 指的就是这第一个参数。

var obj = {
    a: 20
}
function func() {
    console.log(this.a);
}

func.call(obj);    // 20
func.apply(obj);    // 20

不同的是,apply的第二个参数是一个数组或者类数组对象,而call的是指定的参数列表。

五、箭头函数调用

箭头函数有两个作用,更简短的函数并且不绑定this。即箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承 this 。简单点来说,箭头函数的 this 会继承外层不为箭头函数的函数的 this

var a = 20;
function func() {
    var fun1 = () => {
        console.log(this.a)
    }
    return fun1
}

func()();    // 20,箭头函数fun1的this会从外层不为箭头函数的函数func继承而来。func的this指向了window,所以this.a是20

第一个只出现一次的字符

题目描述

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

s = "abaccdeff"
返回 "b"

s = "" 
返回 " "

限制:

0 <= s 的长度 <= 50000

解法:哈希

/**
 * @param {string} s
 * @return {character}
 */
const firstUniqChar = (s) => {
  if(!s) return " "

  let list = []
  let map = {}

  for(let i = 0, len = s.length; i < len; i++) {
    map[s[i]] = map[s[i]] === undefined ? 1 : 0
  }

  for(let item in map) {
      if(map[item] === 1) {
          return item
      }
  }

  return  " "
}

环形链表

题目描述

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

image

例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

image

例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

image

解法一,快慢指针

使用两个指针,一个指针走一步,另外一个指针走两步,如果链表有环的话,那么快慢指针总会相遇的。如果无环,快慢指针永远不会相遇。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {boolean}
 */
/**
 * 快慢指针
 * slow每次移动一步,fast每次移动两步
 */
var hasCycle = function(head) {
    let slow = head
    let fast = head

    while(slow && fast && fast.next) {
        slow = slow.next
        fast = fast.next.next

        if(slow === fast) {
            return true
        }
    }

    return false
};

解法二,哈希表(推荐)

使用 ES6 的新特性 Map 结构

缺点:需要额外的空间来存储访问过的节点

/**
 * 哈希表
 */
var hasCycle = function(head) {
    if (!head) return false;

    const map = new Map();
    let node = head;
    map.set(node, true);

    while (node.next) {
        // 判断哈希表是否已经存在链表节点
        if (map.get(node.next)) {
            return true;
        }
        map.set(node.next, true);
        node = node.next;
    }
    // map.clear()
    return false;
};

解法三,直接判断下一个节点会不会是null

暴力一点,直接遍历链表的所有节点。如果有环的话,那么会一直遍历下去。无环的话就会存在下一个节点为空节点的情况

环形链表

剑指offer-javascript版-二维数组中的查找

对应LeetCode上的题目:面试题04. 二维数组中的查找

题目描述:在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解法一,暴力求解法

因为是 n * m 的二维数组,所以我们可以直接使用双层遍历来遍历整个二维数组的项。注意,该方法不管你数组是递增还是递减的规律,直接一梭子遍历就完事了

var findNumberIn2DArray = function(matrix, target) {
    // 多少行
    const rowLength = matrix.length

    if(!rowLength) return false

    // 多少列
    const colLength = matrix[0].length

    if(!colLength) return false

    for(let i = 0; i < rowLength; i++) {
        for(let j = 0; j < colLength; j++) {
            if(matrix[i][j] === target) {
                return true
            }
        }
    }

    return false
};

当然,我们也可以使用数组提供的 API 去解决这道问题,例如:indexOfincludes,但是由于该种做法过于简单,我们刷题的时候应该尽量避免使用此类 API,下面讲述一下思路:

  • 遍历数组的每一个项,再使用数组的每一项去判断子数组中是否存在目标元素(indexOf 判断子数组中目标元素的位置,可以用是否大于 -1 来判断,includes 则是判断数组中是否存在某元素,存在则返回 true,否则返回 false

解法二,发现规律

从题目描述中可以发现二维数组中的每一行都按照从左到右递增,每一列都按照从上到下递增。我们可以利用这一规律作为解题的突破口,那么

  • 从数组中选取一个数组,分三种情况来分析查找的过程

  • 如果数组中选取的数字刚好等于目标元素,那么直接返回 true,结束查找。

  • 如果选取的数字小于目标元素,那么目标元素的位置应该在选取的数字的右边或者下边

  • 同样,如果选取的数字大于目标元素,那么目标元素的位置应该在选取的数字的左边或者上边

var findNumberIn2DArray = function(matrix, target) {
    const rowLength = matrix.length
    if(!rowLength) {
        return false
    }

    const colLength = matrix[0].length
    if(!colLength) {
        return false
    }

    let row = 0, col = colLength - 1;

    while(row < rowLength && col >= 0) {
        if(matrix[row][col] === target) {
            // 第一种情况
            return true
        } else if(matrix[row][col] > target) {
            // 第三种情况
            --col
        } else {
            // 第二种情况
            ++row
        }
    }

    return false
};

数组中重复的数字

题目描述

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字

示例:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2  3 

解法一,排序

先排序,之后判断是否存在当前项是否和下一项相等

/**
 * @param {number[]} nums
 * @return {number}
 */
// 解法一
let findRepeatNumber = (nums) => {
    nums.sort((a, b) => a - b)

    for(let i = 0, len = nums.length; i < len-1; i++) {
      if(nums[i] === nums[i+1]) {
        return nums[i]
      }
    }
}

解法二,哈希表

遍历一次数组,判断当前项是否已经存在哈希表里面,存在则返回。不存在则加入哈希表

// 解法二
let findRepeatNumber = (nums) => {
  const obj = {}
  for(let i = 0; i < nums.length; i++) {
    if(obj[nums[i]]) {
      return nums[i]
    } else {
      obj[nums[i]] = 1
    }
  }
}

解法三,判断当前项 m 是否与 第 m 项相等

遍历数组,扫描到下标为 i 的数字时,先比较这个数字(用 m 表示)是否等于 i,是的话接着遍历下一个数字。不是的话,拿数字 m 与下标为 m 的数字比较,如果相等,则找到了重复的数字。不相等的话,那么交换数字 m 与 下标为 m 的数字。重复这个过程

// 解法三
const findRepeatNumber = (nums) => {
  for(let i = 0; i < nums.length; i++) {
    while(nums[i] !== i) {
        console.log(nums[i], i)
      if(nums[i] === nums[nums[i]]) {
        return nums[i]
      }

      // 交换数组两个位置上的项,这里是i 和 num[i] 交换
      let temp= nums[i]
      nums[i] = nums[temp]
      nums[temp] = temp
    }
  }
}

树的遍历

想普及一下树的知识,个人认为树是算法必须要牢牢掌握的一部分。

一般树的逻辑可以说是灰常滴简单了:

  • 除根节点之外的每个节点只有一个父节点,根节点没有父节点。

  • 除叶节点之外所有节点都有一个或多个子节点,叶节点没有子节点。

父节点和子节点之间是用指针连接,所以树会涉及到大量的指针,因此与树有关的面试题都不太容易,但是越不容易的知识点,我们就越要攻克。

树的遍历

但是面试时候提到的树,大部分都是二叉树。二叉树是树的一种特殊结构,在二叉树中每个节点最多只能有两个子节点。在二叉树中最重要的考点莫过于树的遍历,即按照某一个顺序遍历访问树中的所有节点。

二叉树的遍历可以有以下几种:

前序遍历

  • 前序遍历:先访问根节点,再访问左节点,最后访问右节点
// 递归版本
const preOrderTraverse = (root) => {
    let list = []

    const preOrder = (node) => {
        if(node !== null) {
            // 先访问根节点
            list.push(node.val)
            // 再访问左节点
            preOrder(node.left)
            // 最后访问右节点
            preOrder(node.right)
        }
    }
    preOrder(root)

    return list
}

// 非递归版本
const preOrderTraverseUnRecur = (root) => {
    let list = [];
    let stack = [root];

    while(stack.length !== 0) {
        const curNode = stack.pop()

        const left = curNode.left
        const right = curNode.right

        // 第一步的时候,先访问的是根节点
        list.push(curNode.val)

        if(right) {
            stack.push(right)
        }

        // 因为pop是取出最后一个元素,所以我们要确保首先拿到的是左节点
        if(left) {
            stack.push(left)
        }
    }
}

中序遍历

  • 中序遍历:先访问左节点,再访问中节点,最后访问右节点
// 递归版本
const inOrderTraverse = (root) => {
    let list = []

    const inOrder = (node) => {
        if(node !== null) {
            inOrder(node.left)
            list.push(node.val)
            inOrder(node.right)
        }
    }

    inOrder(root)

    return list
}

// 非递归版本
const inOrderTraverseUnRecur = (root) => {
    let list = []
    // 借助了栈,先进后出的概念
    let stack = []
    let head = root

    while(stack.length !== 0 || head !== undefined) {
        while(head !== undefined) {
            stack.push(head)
            head = head.left
        }

        if(stack.length !== 0) {
            head = stack.pop()
            list.push(head.val)
            head = head.right
        }
    }

    return list
}

后序遍历

  • 后序遍历:先访问左节点,再访问右节点,最后访问根节点
// 递归
const postOrderTraverse = (root) => {
    let list = []

    const postOrder = (node) => {
        if(!node) {
            postOrder(node.left)
            postOrder(node.right)
            list.push(node.val)
        }
    }

    postOrder(root)

    return list
}

// 非递归
const postOrderTraverseUnRecur = (root) => {
    let list = []
    if(root !== undefined) {
        let s1 = []
        let s2 = []

        s1.push(root)

        while(s1.length !== 0) {
            head = s1.pop()
            s2.push(head)

            if(head.left !== undefined) {
                s1.push(head.left)
            }

            if(head.right !== undefined) {
                s1.push(head.right)
            }
        }

        while(s2.length !== 0) {
            var item = s2.pop()
            list.push(item.val)
        }
    }

    return list
}

为了方便记忆,可以这样记:前序遍历对应的前是根节点最先访问。中序遍历对应的前是根节点放在中间访问。后序遍历对应的前是根节点最后才访问。

这三种遍历方式都有 递归循环 两种不同的实现方式,但是面试官更喜欢考我们的是循环实现方式,所以我们应该要掌握这三种遍历的六种遍历方式!

广度优先遍历

  • 广度优先遍历:即先访问树的第一层节点,接着访问第二层节点...一直访问到最后一层节点

深度优先遍历

  • 深度优先遍历:即先访问树的根节点,再访问根节点的子节点,先往深层访问,深层访问节点结束了,再返回访问同层节点,之后再继续往深层访问节点...

二叉搜索树

二叉搜索树:左子节点总是小于或等于根节点,而右节点总是大于或等于根节点。可以在 O(logn) 内根据数值在二叉搜索树中找到一个节点

:分为最大堆和最小堆。最大堆中根节点的值最大,在最小堆中根节点的值最小。需要快速找到最大值或最小值都可以用堆来解决

红黑树

红黑树:把树中的节点定义为红,黑两种颜色,并通过规则确保从根节点到叶节点的最长路径的长度不超过最短路径的两倍

对称二叉树

题目描述

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

解法一,递归

比较二叉树的 前序遍历对称前序遍历 来判断是不是对称的。

前序遍历:根-左-右

对称前序遍历:根-右-左

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
const isSymmetric = (root) => {
  if(root === null) return true
  return isSymmetrical(root, root)
};

const isSymmetrical = (r1, r2) => {
  if(r1 === null && r2 === null) return true

  if(r1 === null || r2 === null) return false

  if(r1.val !== r2.val) return false

  return isSymmetrical(r1.left, r2.right) && isSymmetrical(r1.right, r2.left)
}

解法二,迭代

和递归的**差不多

const isSymmetric = (root) => {
  if(root === null) return true

  const list = []

  list.push([root.left, root.right])

  while(list.length > 0) {
    const track = list.pop()

    if(track[0] !== null && track[1] !== null) {
      if(track[0].val !== track[1].val) return false
        
      list.push([track[0].left, track[1].right])
      list.push([track[0].right, track[1].left])
    } else if((track[0] !== null && track[1] === null) || (track[0] === null && track[1] !== null)) {
      return false
    }
  }
  return true
}

二叉树的层次遍历II

题目描述

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:
二叉树:[3,9,20,null,null,15,7] ,

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

这道题和二叉树的层次遍历相似,只需要把 reverse() 去除就可以了

解法一:广度优先遍历

var levelOrder = function(root) {
    if(!root) return []
    let res = [], 
        queue = [root]
    while(queue.length) {
        let curr = [],
            temp = []
        while(queue.length) {
            let node = queue.shift()
            curr.push(node.val)
            if(node.left) temp.push(node.left)
            if(node.right) temp.push(node.right)
        }
        res.push(curr)
        queue = temp
    }
    return res
};

解法二:深度优先遍历

var levelOrder = function(root) {
    const res = []
    var dep = function (node, depth){
        if(!node) return
        res[depth] = res[depth]||[]
        res[depth].push(node.val)
        dep(node.left, depth + 1)
        dep(node.right, depth + 1)
    }
    dep(root, 0)
    return res
};

二叉树的最近公共祖先

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科 中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4]

image

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3

示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
``

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。

## 解法

```js
/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    
};

leetcode-javascript版-合并两个排序的链表

题目描述

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

解法一,递归

拿两个链表的头结点做比较,取较小的节点作为头结点,头结点(较小的节点)的next则等于小节点的next和大节点之间比较的较小值,依次递归。最后返回头结点。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    if(!l1) return l2
    if(!l2) return l1

    let head

    if(l1.val < l2.val) {
        // 链表1先排前面
        head = l1
        // 继续递归
        head.next = mergeTwoLists(l1.next, l2)
    } else {
        // 链表2先排前面
        head = l2
        // 继续递归
        head.next = mergeTwoLists(l1, l2.next)
    }

    return head
};

剑指offer-javascript版-删除链表的节点

题目描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。

例:

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

例:

输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.

解法一

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var deleteNode = function(head, val) {
    let pre = new ListNode(-1)
    pre.next  = head
    let cur = pre

    while(cur) {
        if(cur.next.val === val) {
            cur.next = cur.next.next
            return pre.next
        }

        cur = cur.next
    }
};

剑指offer-javascript版-重建二叉树

对应LeetCode上的题目:面试题07. 重建二叉树

题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例子

给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7

解法:二叉树的前序遍历和中序遍历

二叉树的前序遍历中,第一个数字是二叉树的根节点。在中序遍历中,根节点位于序列的中间,即根节点的左边是左子树节点的值,右边是右子树节点的值。

由前序遍历 [3,9,20,15,7] 可知,3 是根节点。中序遍历 [9,3,15,20,7] 可知,根节点 3 的左边是左子树节点 9,右边是右子树节点 15207。所以

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */

var buildTree = function(preorder, inorder) {
    if(!preorder.length || !inorder.length) {
        return null
    }

    const rootVal = preorder[0]
    const node = new TreeNode(rootVal)

    // i有两个含义,一个是根节点在中序遍历结果中的下标, 另一个是当前左子树的节点个数
    let i = 0;
    for(; i < inorder.length; ++i) {
        if(inorder[i] === rootVal) {
            break;
        }
    }

    // i主要是求出根节点在中序遍历序列中的位置。那么i位置前面的都是左子树的值,后边的都是右子树的值。
    // 中序遍历和前序遍历的左子树和右子树节点的个数都分别相等
    // preorder.slice(1, i+1) 在前序遍历里面,左节点有多少个
    // inorder.slice(0, i) 在中序遍历里面,左节点就是根节点位置i前面的那些
    node.left = buildTree(preorder.slice(1, i+1), inorder.slice(0, i))
    node.right = buildTree(preorder.slice(i+1), inorder.slice(i + 1))

    return node
};

一文学会 react hook

一文学会 react hook

前言

hook 是什么

state hook 是什么

effect hook 是什么

hook 有哪些规则

怎么自定义 hook

hook 的其它API

前端异常处理

学习 如何优雅处理前端异常?(史上最全前端异常处理方案) 后的笔记

前端异常监控

为什么要处理异常

  • 增强用户体验
  • 远程定位问题
  • 未雨绸缪,及时发现问题
  • 无法复线问题,尤其是移动端,机型,系统都是问题
  • 完善的前端方案,前端监控系统

前端异常分类

  • JS语法错误、代码异常
  • AJAX 请求异常
  • 静态资源加载异常
  • Promise异常
  • iframe异常
  • 跨域 script error
  • 崩溃和卡顿

try-catch 误区

try-catch 只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到

window.onerror 不是万能的

JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。不论是静态资源异常,或者接口异常,错误都无法捕获到。window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx

需要注意:

  • onerror 最好写在所有 JS 脚本的前面,否则有可能捕获不到错误;
  • onerror 无法捕获语法错误;

在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,不过(至少在 Firefox 中)能被单一的window.addEventListener 捕获。这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

需要注意:

  • 不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。
  • 需要注意避免 addEventListener 重复监听。

Promise catch

在 promise 中使用 catch 可以非常方便的捕获到异步 error ,这个很简单。

没有写 catch 的 Promise 中抛出的错误无法被 onerror 或 try-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。

解决方案:为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听Uncaught Promise Error。使用方式:

window.addEventListener("unhandledrejection", function(e){
  console.log(e);
});

vue

Vue.config.errorHandler = (err, vm, info) => {
  console.error('通过vue errorHandler捕获的错误');
  console.error(err);
  console.error(vm);
  console.error(info);
}

React

React 异常捕获 React 16 提供了一个内置函数 componentDidCatch,使用它可以非常简单的获取到 react 下的错误信息

除此之外,我们可以了解一下:error boundary UI 的某部分引起的 JS 错误不应该破坏整个程序,为了帮 React 的使用者解决这个问题,React 16 介绍了一种关于错误边界(error boundary)的新观念。

需要注意的是:error boundaries 并不会捕捉下面这些错误。

  • 事件处理器
  • 异步代码
  • 服务端的渲染代码
  • 在 error boundaries 区域内的错误

componentDidCatch() 方法像 JS 的 catch{} 模块一样工作,但是对于组件,只有 class 类型的组件(class component )可以成为一个 error boundaries 。

实际上,大多数情况下我们可以在整个程序中定义一个 error boundary 组件,之后就可以一直使用它了!

iframe 异常

对于 iframe 的异常捕获,我们还得借力 window.onerror:

window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}

script error

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。这时候,是不会有其他太多辅助信息的,但是解决思路无非如下:

跨源资源共享机制( CORS ):我们为 script 标签添加 crossOrigin 属性。或者动态去添加 js 脚本。特别注意,服务器端需要设置:Access-Control-Allow-Origin

崩溃和卡顿

卡顿也就是网页暂时响应比较慢, JS 可能无法及时执行。但崩溃就不一样了,网页都崩溃了,JS 都不运行了,还有什么办法可以监控网页的崩溃,并将网页崩溃上报呢?

崩溃和卡顿也是不可忽视的,也许会导致你的用户流失。

  1. 利用 window 对象的 load 和 beforeunload 事件实现了网页崩溃的监控。

  2. 基于以下原因,我们可以使用 Service Worker 来实现网页崩溃的监控:
    Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。

错误上报

  1. 通过 Ajax 发送数据 因为 Ajax 请求本身也有可能会发生异常,而且有可能会引发跨域问题,一般情况下更推荐使用动态创建 img 标签的形式进行上报。

  2. 动态创建 img 标签的形式

收集异常信息量太多,怎么办?实际中,我们不得不考虑这样一种情况:如果你的网站访问量很大,那么一个必然的错误发送的信息就有很多条,这时候,我们需要设置采集率,从而减缓服务器的压力:

Reporter.send = function(data) {
  // 只采集 30%
  if(Math.random() < 0.3) {
    send(data)      // 上报错误信息
  }
}

采集率应该通过实际情况来设定,随机数,或者某些用户特征都是不错的选择。

总结

如何优雅的处理异常呢?

  • 可疑区域增加 Try-Catch
  • 全局监控 JS 异常 window.onerror
  • 全局监控静态资源异常 window.addEventListener
  • 捕获没有 Catch 的 Promise 异常:unhandledrejection
  • VUE errorHandler 和 React componentDidCatch
  • 监控网页崩溃:window 对象的 load 和 beforeunload
  • 跨域 crossOrigin 解决

正如上文所说:采用组合方案,分类型的去捕获异常,这样基本 80%-90% 的问题都化于无形。

参考链接:如何优雅处理前端异常?

O(1) 时间插入、删除和获取随机元素 - 允许重复

题目描述

设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。

注意: 允许出现重复元素。

insert(val):向集合中插入元素 val
remove(val):当 val 存在时,从集合中移除一个 val
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。

示例:

// 初始化一个空的集合。
RandomizedCollection collection = new RandomizedCollection();

// 向集合中插入 1 。返回 true 表示集合不包含 1 。
collection.insert(1);

// 向集合中插入另一个 1 。返回 false 表示集合包含 1 。集合现在包含 [1,1] 。
collection.insert(1);

// 向集合中插入 2 ,返回 true 。集合现在包含 [1,1,2] 。
collection.insert(2);

// getRandom 应当有 2/3 的概率返回 1 ,1/3 的概率返回 2 。
collection.getRandom();

// 从集合中删除 1 ,返回 true 。集合现在包含 [1,2] 。
collection.remove(1);

// getRandom 应有相同概率返回 1 和 2 。
collection.getRandom();

解法:哈希表

let RandomizedSet = function() {
    this.list = []
    this.map = {}
};

RandomizedSet.prototype.insert = function(val) {
    if(this.map[val]) return false

    this.list.push(val)
    this.map[val] = this.list.length
    return true
};

RandomizedSet.prototype.remove = function(val) {
    if(!this.map[val]) return false

    const final = this.list[this.list.length-1]

    // 下面这块主要是把要数组的尾值赋给对应的val值的位置,之后把数组最后的值踢出去即可
    const index = this.map[val]
    // 这里的index-1即我们拿到的index不是数组的下标,还需要减1。
    this.list[index-1] = final
    this.map[final] = index

    delete this.map[val]
    this.list.pop()

    return true
};

RandomizedSet.prototype.getRandom = function() {
    const index = Math.floor(Math.random() * this.list.length)
    return this.list[index]
};

JavaScript深入理解之手写代码

在面试中,面试官多多少少都会要求面试者手写几道手写代码方法,这时候,不仅要求面试者能够熟练使用这些方法,更是要求面试者掌握这些方法的原理。

  • 实现一个 new 操作符

  • 实现一个 instanceof

  • 实现一个浅拷贝或者深拷贝

  • 实现 call 和 apply

  • 实现 防抖 和 节流

  • 实现一个 promise

  • 实现 ajax 原理

  • 实现 jsonp

  • 实现 JSON.stringify

  • 实现 JSON.parse

  • 实现 flat 扁平化

  • 实现类的继承

  • 实现双向绑定

  • 实现函数柯里化

实现一个 new 操作符

要创建一个实例,那么必须要使用 new 操作符。使用 new 操作符内部会经历4个过程

  1. 创建一个新的对象

  2. 将构造函数的作用域赋给了新对象

  3. 执行构造函数中的代码

  4. 返回新的对象***(注意:如果构造函数中有返回对象类型的值,那么我们就直接返回这个对象。否则返回新的对象)***

function createNew(fn) {
    // 1. 创建一个新的对象
    let obj = new Object()

    // 2. 将构造函数的作用域赋给了新对象
    if(fn.prototype) {
        obj.__proto__ = fn.prototype
    }

    // 3. 执行构造函数中的代码
    const result = fn.apply(obj, [...arguments].slice(1))

    // 4. 返回新的对象
    return typeof result === 'object' ? result : obj
}

实现一个 instanceof

instanceof 可以正确的判断对象的类型。因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

// letf 对象
// right 类型
const instanceof = (left, right) => {
    // 类型的原型
    let prototype = right.prototype

    // 对象的原型
    let proto = left.__proto__

    while(true) {
        if(proto === null) return false

        if(proto === prototype) return true

        proto = proto.__proto__
    }

    return false
}

实现一个浅拷贝或者深拷贝

在使用对象的时候,我们经常会碰到这样的一个问题

const a = {
    name: 'xiaohong'
}

const b = a

a.name = 'xiaoming'
console.log(b)  // {name: 'xiaoming'}

可能会有朋友会问,咦,我们不是没有修改对象 b 的属性吗,怎么属性 name 变了。其实这里面涉及到引用类型的问题。对象是属于引用类型,把对象赋值给另外一个变量,实际上是把对象的指针指向的内存地址共享给了变量。对象和变量共享同一份内存地址。那么 a 的属性改变了,那么 b 的也会跟着变化的。这时候为了避免这种情况发生,我们可以使用 浅拷贝 来进行操作

  • Object.assign
const a = {
    name: 'xiaohong'
}

const b = Object.assign({}, a)

a.name = 'xiaoming'
console.log(b)  // {name: 'xiaohong'}
  • 对象展开运算符
const a = {
    name: 'xiaohong'
}

const b = {...a}

a.name = 'xiaoming'
console.log(b)  // {name: 'xiaohong'}

注意,浅拷贝 避免对象内部的属性不是引用类型的情况,那么如果对象的内部属性仍然是引用类型的情况时,浅拷贝 这时候只会对对象最外层生效了

const a = {
    name: 'xiaohong',
    obj: {
        age: 10
    }
}

const b = Object.assign({}, a)

a.obj.age = 20
console.log(b)  // {name: 'xiaohong',obj:{age: 20}}

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入 深拷贝 的概念了。

一般来说,使用 JSON.parse(JSON.stringify()) 已经满足了

var newObj = JSON.parse( JSON.stringify( someObj ) );

但是这种方法存在局限性

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

所以,我们可以利用递归来解决这种情况

function deepCopy(obj){
    //判断是否是简单数据类型,
    if(typeof obj == "object"){
        //复杂数据类型
        var result = obj.constructor == Array ? [] : {};
        for(let i in obj){
            result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
    }else {
        //简单数据类型 直接 == 赋值
        var result = obj;
    }
    return result;
}

实现 call 和 apply

call 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。与 apply 方法类似,只有一个区别,就是 call 方法接受的是一个参数列表,而 apply 方法接受的是一个包含多个参数的数组。

Fuction.prototype.myCall = (content) => {
    content = content || window

    content.fn = this

    let args = [...arguments].slice(1)
    let res = content.fn(...args)

    delete content.fn

    return res
}

同理,apply 的实现只是传入的参数是一个数组而已

Fuction.prototype.myApply = (content) => {
    content = content || window

    content.fn = this

    let res
    if(argument[1]) {
        res = content.fn(...arguments[1])
    } else {
        res = content.fn()
    }

    delete content.fn

    return res
}

实现 防抖 和 节流

防抖 指的是防止用户用户过多操作,而带来不必要的性能耗费,把操作改成最后一次执行,只要用户有操作,事件行为也会一直被推迟。例如输入框

const debounce = function (wait, fn) {
    let timer

    return (...args) => {
        // 如果事件一直在触发,那么就把上一次事件的定时器清除
        if(timer) {
            clearTimeOut(timer)
        }

        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

防抖节流 本质是不一样的。防抖 是将多次执行变为最后一次执行,节流 是将多次执行变成每隔一段时间执行。

const throttle = function (wait, fn) {
    let preTime = 0

    return (...args) => {
        // 获取当前的时间戳
        let curTime = new Date().getTime()

        // 与上一个时间段的时间戳做对比
        if((curTime - preTime) > wait) {
            preTime = curTime
            fn.apply(this, args)
        }
    }
}

实现一个promise

(1)"promise"是一个对象或者函数,该对象或者函数有一个then方法

(2)"then"是一个对象或者函数,用来定义then方法

(3)"value"是promise状态成功时的值

(4)"reason"是promise状态失败时的值

普通版

function myPromise(constructor) {
    let self = this

    // 一个promise必须有3个状态,pending,fulfilled(resolved)
    self.status = "pending" // 定义状态改变前的初始状态
    self.value = undefined  // 定义状态为resolve的时候的状态
    self.reason = undefined // 定义状态为reject的时候的状态

    function resolve () {
        // 两个pending,保证了状态的改变是不可逆的
        if(selft.status === 'pending') {
            self.value = value
            self.status = "resolved"
        }
    }

    function reject () {
        // 两个pending,保证了状态的改变是不可逆的
        if(selft.status === 'pending') {
            self.reason = reason
            self.status = "rejectd"
        }
    }

    //捕获构造异常
    try{
        constructor(resolve, reject)
    } catch(e) {
        reject(e)
    }
}

// 一个promise必须有一个then方法,then方法接受两个参数
myPromise.prototype.then = function(onFullfilled, onRejected) {
    let self = this

    switch(self.status) {
        case "resolved":
            onFullfilled(self.value)
            break
        case "rejected":
            onRejected(self.value)
            break
        default
    }
}

但是这里 myPromise 无法处理异步的 resolve. 比如:

var p= new myPromise(function(resolve,reject){
    setTimeout(function(){
        resolve(1)
    },1000)
});

p.then(function(x){
    console.log(x)
})

而且 myPromise 也没有实现链式调用,也就是说 then 方法返回的应该是一个 promise

// 为了处理异步resolve,我们修改myPromise的定义,用2个数组onFullfilledArray和onRejectedArray来保存异步的方法。在状态发生改变时,一次遍历执行数组中的方法。
function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    self.onFullfilledArray=[];
    self.onRejectedArray=[];
    function resolve(value){
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
          self.onFullfilledArray.forEach(function(f){
                f(self.value);
                //如果状态从pending变为resolved,
                //那么就遍历执行里面的异步方法
          });
       }
    }
    function reject(reason){
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
          self.onRejectedArray.forEach(function(f){
              f(self.reason);
             //如果状态从pending变为rejected, 
             //那么就遍历执行里面的异步方法
          })
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

// 要通过then方法实现链式调用,那么也就是说then方法每次调用需要返回一个primise,同时在返回promise的构造体里面,增加错误处理部分,我们来改造then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                try{
                   let temple=onFullfilled(self.value);
                   resolve(temple)
                }catch(e){
                   reject(e) //error catch
                }
             });
             self.onRejectedArray.push(function(){
                 try{
                   let temple=onRejected(self.reason);
                   reject(temple)
                 }catch(e){
                   reject(e)// error catch
                 }
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
            try{
              let temple=onFullfilled(self.value);
              //将上次一then里面的方法传递进下一个Promise的状态
              resolve(temple);
            }catch(e){
              reject(e);//error catch
            }
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
            try{
               let temple=onRejected(self.reason);
               //将then里面的方法传递到下一个Promise的状态里
               resolve(temple);
            }catch(e){
               reject(e);
            }
        })
        break;
      default:
   }
   return promise2;
}

实现ajax原理

let xhr = new XMLHttpRequest()

xhr.open('get/post', '/api/getSth',  true)

xhr.send(null)

xhr.onreadystatechange = () => {
  if(xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      console.log(xhr.responseText)
    } else {
      console.log('Error:' + xhr.status)
    }
  }
}

实现 jsonp

function objectToQuery(obj) {
  const arr = []

  for(let i in obj) {
    arr.push(encodeURIComponent(i)+ '=' + encodeURIComponent(o[i]))
  }

  return arr.join('&')
}

function jsonp({url, data, callback}) {
  const container = document.getElementsByTagName('head')[0]

  const fnName = `jsonp_${new Date().getTime()}`
  const script = document.createElement('script')
  script.src = `${url}?${objectToQuery(data)}&callback=${fnName}}`
  script.type = 'type/javascript'
  container.appendChild(script)
  
  window[fnName] = function (res) {
    callback && callback(res)

    container.removeChild(script)
    delete window[fnName]
  }

  script.onerror = function() {
    window[fnName] = function () {
      callback && callback('error')

      container.removeChild(script)
      delete window[fnName]
    }
  }
}

剑指offer-javascript版-替换空格

对应LeetCode上的题目:面试题05. 替换空格

题目描述:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

例:

输入:s = "We are happy."
输出:"We%20are%20happy."

解法一,正则表达式

字符串有一个 replace 方法,可以把字符串里面特定的字符转换成我们想要的字符

var replaceSpace = function(s) {
    if(!s) return

    return s.replace(/ /g, '%20')
};

一句代码就搞定,这方法真香

解法二

使用字符串的分割 split 和 数组的分割 join

var replaceSpace = function(s) {
    if(!s) return

    return s.spilt(' ').join('%20')
};

注意,虽然这种 api 是香,但是只能作为一种辅助的方法,面试官更多想考我们的算法能力和解题思路,这种只能用来当作题目的一种解法

解法三,双指针

遍历原字符串,统计空格和非空格字符个数,计算替换后的新字符的长度,准备两个指针,指针 i 指向原字符串,指针 j 指向新字符串

var replaceSpace = function(s) {
    if(!s || !s.length) {
        return ""
    }

    // emptyNum 空格的个数
    // chNum 非空字符的个数
    let emptyNum = 0, chNum = 0;

    for(let i = 0, len = s.length; i < len; i++) {
        if(s[i] === ' ') {
            ++emptyNum
        } else {
            ++chNum
        }
    }

    const length = emptyNum * 2 + chNum
    const chs = new Array(length)
    // i 是新字符串的下标,j是源字符串的下标
    for(let i = 0, j = 0; j < s.length; ++j) {
        if(s[j] === " ") {
            chs[i++] = "%"
            chs[i++] = "2"
            chs[i++] = "0"
        } else {
            chs[i++] = s[j]
        }
    }

    return chs.join("")
}

面试题05. 替换空格

二叉树的最大深度

题目描述

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
     / \
    15   7

返回它的最大深度 3

解法一,深度优先遍历 + 递归

主要考察的是深度优先遍历和递归

function TreeDepth(node) {
    // 最大深度等于左子树或者右子树的最大值加1
    return !node ? 0 : Math.max(TreeDepth(node.left), TreeDepth(node.right)) + 1
}

最小的k个数

题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

限制:

0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

leetcode来源:最小的k个数

解答一,sort 排序 + 分割

const getLeastNumbers = function(arr, k) {
    arr.sort((a, b) => a - b)

    return arr.slice(0, k)
};

解答二,快排

快排**

/**
 *
 * @param {number[]} arr
 * @param {number} start
 * @param {number} end
 * @return {number}
 */
function partiton(arr, start, end) {
    const k = arr[start];
    let left = start + 1,
        right = end;
    while (1) {
        while (left <= end && arr[left] <= k) ++left;
        while (right >= start + 1 && arr[right] >= k) --right;

        if (left >= right) {
            break;
        }

        [arr[left], arr[right]] = [arr[right], arr[left]];
        ++left;
        --right;
    }
    [arr[right], arr[start]] = [arr[start], arr[right]];
    return right;
}

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
const getLeastNumbers = function(arr, k) {
    const length = arr.length;
    if (k >= length) return arr;
    let left = 0,
        right = length - 1;
    let index = partiton(arr, left, right);
    while (index !== k) {
        if (index < k) {
            left = index + 1;
            index = partiton(arr, left, right);
        } else if (index > k) {
            right = index - 1;
            index = partiton(arr, left, right);
        }
    }

    return arr.slice(0, k);
};

解答三,最大堆

function swap(arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]];
}

class MaxHeap {
    constructor(arr = []) {
        this.container = [];
        if (Array.isArray(arr)) {
            arr.forEach(this.insert.bind(this));
        }
    }

    insert(data) {
        const { container } = this;

        container.push(data);
        let index = container.length - 1;
        while (index) {
            let parent = Math.floor((index - 1) / 2);
            if (container[index] <= container[parent]) {
                break;
            }
            swap(container, index, parent);
            index = parent;
        }
    }

    extract() {
        const { container } = this;
        if (!container.length) {
            return null;
        }

        swap(container, 0, container.length - 1);
        const res = container.pop();
        const length = container.length;
        let index = 0,
            exchange = index * 2 + 1;

        while (exchange < length) {
            // 如果有右节点,并且右节点的值大于左节点的值
            let right = index * 2 + 2;
            if (right < length && container[right] > container[exchange]) {
                exchange = right;
            }
            if (container[exchange] <= container[index]) {
                break;
            }
            swap(container, exchange, index);
            index = exchange;
            exchange = index * 2 + 1;
        }

        return res;
    }

    top() {
        if (this.container.length) return this.container[0];
        return null;
    }
}

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
const getLeastNumbers = function(arr, k) {
    const length = arr.length;
    if (k >= length) {
        return arr;
    }

    const heap = new MaxHeap(arr.slice(0, k));
    for (let i = k; i < length; ++i) {
        if (heap.top() > arr[i]) {
            heap.extract();
            heap.insert(arr[i]);
        }
    }
    return heap.container;
};

参考来源:https://xxoo521.com/2020-02-21-least-nums/

JavaScript深入理解之继承

本文是《Javascript高级程序设计》的笔记汇总

对于使用过基于类的语言(如java或C++) 的开发人员来说,javascript有点迷,因为它的动态的,并且本身不提供一个class实现。虽然ES6中引入了 class 关键字,但是这只是语法糖,javascript仍然是基于原型实现的。

1.基于原型链的继承

每个实例对象(Object)都有一个私有属性(称之为__proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null,根据定义,null 没有原型,并作为这个原型链中的最后一个环节

function Parent() {
    this.name = 'jane'
    this.list = [1,2]
}

Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child() {

}

// Child的原型是Parent的实例
Child.prototype = new Parent()

let child1 = new Child()
child1.name = "tony"
child1.list.push(3)
console.log(child1.name)    // tony
console.log(child1.list)    // [1,2,3]

let child2 = new Child()
console.log(child2.name)    // jane
// 引用类型的属性被所有实例共享
console.log(child2.list)    // [1,2,3]

缺点:

  • 引用类型的属性被所有实例共享
  • 在创建 Child 的实例时,不能向Parent传参

2.构造函数继承

即在子类型构造函数内部调用父类型(超类型)构造函数,因此可以使用apply()call() 方法在新创建的对象上执行构造函数

function Parent(name) {
    this.name = name
    this.list = [1,2]
}

function Child(name) {
    Parent.call(this, name)
}

let child1 = new Child('tony')
child1.list.push(3)
console.log(child1.name)    // tony
console.log(child1.list)    // [1,2,3]

let child2 = new Child('jane')
console.log(child2.name)    // jane
// 引用类型的属性被所有实例共享
console.log(child2.list)    // [1,2]

优点:

  • 避免了引用类型的属性被所有实例共享
  • 可以在Child中向Parent传值

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法

3.组合继承(原型继承+构造函数继承)

指的是将原型继承和构造函数继承组合到一起,从而发挥二者之长

function Parent(name) {
    this.name = name
    this.list = [1,2]
}

function Child(name) {
    // 构造函数继承
    Parent.call(this, name)
}

// 原型继承
Child.prototype = new Parent()
Child.prototype.constructor = Child;
Child.prototype.getName = function(){
    console.log(this.name)
}

let child1 = new Child('tony')
child1.list.push(3)
console.log(child1.name)    // tony
console.log(child1.list)    // [1,2,3]

let child2 = new Child('jane')
console.log(child2.name)    // jane
// 引用类型的属性被所有实例共享
console.log(child2.list)    // [1,2]

避免了原型链继承和构造函数继承的缺陷,融合了它们的优点,是最常用的继承模式

4.原型式继承

借助原型可以基于已有的对象创建新对象,同时不用创建自定义类型

function object(o) {
    function F(){}

    F.prototype = o;

    return new F()
}

ES6新增的Object.create() 和 object 方法的行为一样

缺点:

  • 引用类型值的属性始终都会共享相应的值,就像原型模式一样

5.寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

缺点:

  • 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

6.寄生组合式继承

组合继承是最常用的继承模式,但是组合继承也有缺点,那就是会调用两次超类型构造函数

  1. 创建子类型原型的时候

  2. 子类型构造函数内部

function Parent(name) {
    this.name = name
    this.list = [1,2]
}

function Child(name) {
    // 第二次子类型构造函数内部
    Parent.call(this, name)
}

// 第一次创建子类型原型的时候
Child.prototype = new Parent()
Child.prototype.constructor = Child;
Child.prototype.getName = function(){
    console.log(this.name)
}

所谓的寄生组合式继承,就是借用构造函数来继承属性,通过原型链的混成形式来继承方法

function Parent(name) {
    this.name = name
    this.list = [1,2]
}

function Child(name) {
    Parent.call(this, name)
}

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// ES6的写法
// Child.prototype = Object.create(Parent.prototype, {
//   constructor: {
//     value: Child,
//     enumerable: false,
//     writable: true,
//     configurable: true
//   }
// })

// 当我们使用的时候:
prototype(Child, Parent);

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

7. class继承

在ES中,我们可以使用 class 去实现继承

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)

class 的本质就是函数

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.