手写代码

252 阅读36分钟

两数之和

  • 整数数组nums,目标值target,返回下标,假设只对应一个答案
let arr = [1, 4, 7, 2, 9, 10]
let target = 9
const towNums = (nums, target) => {
    let len = nums.length
    for (let i = 0; i < len; i++) {
        for (let j = i + 1; j < len; j++) {//因同一个元素不允许重复出现,所以从i的下一位开始遍历
            if (nums[i] + nums[j] === target) {
                return [i, j]
            }
        }
    }
}

console.log(towNums(arr, target))

call、apply、bind

提供一种灵活的方式来设置函数执行时的上下文,且可在运行时动态的确定参数数量

call

  • 传参用逗号隔开,且会立即执行函数
  • function.call(context,arg1,arg2) call原理思路:
  1. 根据call的规则设置上下文对象,也就是this的指向。
  2. 通过设置context的属性,将函数的this指向[隐式绑定]到context上
  3. 通过隐式绑定执行函数并传递参数。
  4. 删除临时属性,返回函数执行结果
  Function.prototype.myCall = function (context, ...args) {
    //myCall方法将被添加到Function原型对象上,目标函数调用时,myCall内部得到this将指向目标函数
    if (typeof context !== 'function') {
        throw new TypeError('not function')
    }
    context = context || window//如果不传参数,默认指向window
    context.fn = this//将目标函数作为context对象的方法来执行,由此目标函数内的this将指向context
    const result = context.fn(...args)//扩展运算符...处理传入目标函数的参数
    delete context.fn//context中删除目标函数,因为不能改写了对象
    return result
}

apply

  • 传参是数组,且会立即执行函数,this指向由第一个参数决定,第2个参数是数组或argument对象
  • function.apply(context,[arguments])
 // apply原理和call类似、思路:将要改变this指向的方法挂到目标this上执行并返回
//tips:它的调用者必须是函数Function,并且只接受两个参数,第一个参数的规则和call一致
//第二个参数必须是数组或类数组,他们会被转换成类数组,传入Function中,且会被影射到Function对应的参数上、这也是call和apply之间很重要的一个区别
    
  Function.prototype.myApply = function (context, args) {
    if (typeof context !== 'function') {
        throw new TypeError('not function')
    }
    context = context || window
    context.fn = this
    const result = context.fn(...args)
    delete context.fn
    return result
}
 注意:当apply传入的第一个参数为null时,函数体内的this会指向window
  • 用途 1.找出最大值和最小值
let max = Math.max.apply(null, array);//获取数组中最大项
let min = Math.min.apply(null, array);//获取数组中最小项

2. 实现两个数组合并

let arr1 = ['a','b'];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); //["a", "b", 0, 1, 2]

bind

原理分析:

  1. bind方法会返回一个新的函数
  2. 新函数执行时,它的this指向bind函数的第一个参数
  3. 新函数执行时,会将bind函数除第一参数之外的其余参数,与调用新函数时传入的参数合并,供调用的时候使用
  • 传参用逗号隔开,不会立即执行函数,它返回一个新函数,该函数内部的this指向bind()的第一个参数,之后执行新函数相当于执行了目标函数
  • function.bind(thisArg,arg1,arg2)
  • 思路,类似call,但返回的是函数
//第一种方法
 Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('not function')
    }
    let _this = this//有外部thiss,因函数里有返回的函数,执行中易造成this丢失
    let arg = [...arguments].slice(1)//取外部入参,将context后面的参数取出来
    return function F() {
        if (this instanceof F) {//处理函数使用new的情况
            return new _this(...args, ...arguments)
        } else {
            return _this.apply(context, arg.concat(...arguments))//改变this指向,合并函数入参数
        }
    }
}
//第二种方法
 Function.prototype.myBind = function (context, ...initArgs) {
    if (context === undefined || (context === null)) context = window
    const _this = this
    return function (...args) {
        context.fn = _this
        const result = context.fn(...initArgs, ...args)//处理传入目标函数的初始参数,后续参数
        delete context.fn;
        return result
    }
}

反转字符串

  • split()将字符串拆分为子字符串数组
  • reverse:Array.prototype.reverse()反转数组中的元素,并返回同一数组的引用
  • join()把数组中的所有元素转换一个字符串
let str = 'the sky is blue'
const reverseWord = (str) => {
    return str.trim().split(' ').reverse().join(' ')
}
console.log(reverseWord(str))//blue is sky the

排序

标题时间复杂度稳定性空间
冒泡o(n²)稳定o(1)
插入o(n²)稳定o(1)
选择o(n²)稳定o(1)
快排o(nlogⁿ)不稳定o(logⁿ)

空间:o(1)算法执行所需临时空间,不随n大小变化

时间:平方阶o(n²:把o(n)在嵌套循环一遍

常数阶:o(1)无论代码执行多少行,只要无循环等复杂结构

线性阶:o(n)如for循环的代码执行n遍,消耗时间随n变化

对数阶:o(logⁿ)

let arr = [12, 4, 2, 15, 5, 7, 9, 10, 1]

冒泡排序

  1. 将数组中两个相邻元素进行比较,
  2. 对每一队相邻元素作同样的比较,最后会是最大的元素,
  3. 针对所有元素重复以上步骤(除最后一个),直到所有元素排序完
  function bubbleSort(arr) {
    const len = arr.length
    for (let i = 0; i < len - 1; i++) {//外层循环i控制比较的轮数
        for (let j = 0; j < len - 1 - i; j++) {// 里层循环控制每一轮比较的次数j,arr[i] 只用跟其余的len -i-1个元素比较
            if (arr[j] > arr[j + 1])[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
        }
    }
    return arr
}

插入排序

  1. 将数组分为有序和无序
  2. 每次从后面未排序不分从头到尾依次扫描
  3. 把扫描的每个元素插入到有序的适当位置
 function insertSort(arr) {
    const len = arr.length
    for (let i = 1; i < len; i++) {
        //将arr[i]插入到arr[i-1],arr[i-2],arr[i-3]……之中
        for (let j = i; j > 0; j--) {
            if (arr[j] < arr[j - 1]) {
                [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
            }
        }
    }
    return arr
}

快速排序

  1. 从数组中找一个中间项作为基准值,在原有的数组中把它移除
  2. 重新排序数组,准备左右两个数组,循环剩下数组中的每一项,比当前项小的放到左边数组中,反之放到右边数组中
  3. 递归方式让左右两边的数组持续这样处理,一直到左右两边都排好序为止
 function quickSort(arr) {
    if (arr.length <= 1) return arr //结束递归(当ary小于等于一项,则不用处理)
    const middleIndex = Math.floor(arr.length / 2)//中间项
    
    const middle = arr.splice(middleIndex, 1)[0]
    const leftArr = [],
        rightArr = []
    for (let i = 0; i < arr.length; i++) {
        const current = arr[i]
        current < middle ? leftArr.push(current) : rightArr.push(current)
    }
   
    return quickSort(leftArr).concat(middle, quickSort(rightArr))//递归处理,最后左,中间项,右拼接
    }

选择排序

  1. 将数组分成2部分,已排序和未排序
  2. 先在未排序中找到最小元素,放到已排序中首位
  3. 再从剩余未排序中继续找最小,放到已排序的末尾
  4. 重复2直到所有元素排序完
 function selectionSort(arr) {
    const len = arr.length
    for (let i = 0; i < len - 1; i++) {
        let minIndex = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j //保存最小数的下标
            }
        }
        // const temp = arr[i]
        // arr[i] = arr[minIndex]
        // arr[minIndex] = temp
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
    }
    return arr
}

防抖、节流

防抖

是指当事件被触发后,函数不会立即执行,而是等待一段时间,如果在这段时间内没有再次触发该事件,函数才会执行。如果在这段时间内再次触发了该事件,则会重新计算等待时间

  • 单位时间内频繁触发事件,只执行最后一次(重新计时)
  • 场景:
    • 按钮点击:防止误触或者误操作。例如,当用户在短时间内多次点击提交按钮时,防抖技术可以只将其视作一次有效的点击操作
    • 输入框实时搜索:用防抖技术延迟执行搜索查询,减少不必要的查询和服务器压力
    • 窗口大小调整:用防抖技术避免在调整过程中频繁地重新计算布局。这样可以提高性能,减少不必要的计算。
    • 表单验证:用防抖技术在用户停止输入一段时间后再执行验证,减少验证的次数
  • 利用定时器每次触发先清掉以前的定时器重新计时
//第一种方法:最优
 var debounce = function (fn, t) {
    let timer = null//用来保存setTimeout的返回值
    return function (...args) {//返回一个新函数
        if (timer) clearTimeout(timer)//若timer不为null,说明之前已调用过setTimeout,那就清除之前的定时器重新计时
        timer = setTimeout(() => fn(...args), t)//调用setTimeout,在t毫秒后调用fn函数并传入参数
    }
}
//第二种方法
function debounce(fn, t) {
    let timer = null
    return function () {
        if (timer) clearTimeout(timer) //重新计时
        timer = setTimeout(() => {
            fn.apply(this.argument)
            timer = null
        }, t)
    }
}

节流

是指在一定时间内只执行一次函数,即使事件被连续触发

  • 单位时间内频繁触发事件只执行一次
  • 场景:通常用于处理高频事件:
    • **鼠标滑动:**例如实现一个拖拽功能,可以使用节流技术减少鼠标移动事件的处理频率。这样可以降低处理鼠标移动事件的开销,提高性能
    • 滚动事件监听: 用节流技术减少检查滚动位置的频率,提高性能。这样可以避免在滚动过程中频繁地触发加载操作,提高页面的加载效率
    • 动画效果: 当实现一个基于时间的动画效果时,可以使用节流技术限制动画帧率,降低计算开销。这样可以避免动画效果过于复杂而导致的性能问题
    • 窗口调整 页面滚动或者抢购等场景:在这些场景中需用节流来控制事件的执行频率,以防止资源的浪费
  • 利用定时器等定时器执行完才开启定时器
function throttle(fn, t) {
    let timer = null
    return function () {
        if (timer) return //规定时间内只执行一次
        timer = setTimeout(() => {
            fn.apply(this.argument)
            timer = null
        }, t)
    }
}

两者区别

  1. 执行时机不同:防抖是在最后一次触发事件之后等待一段时间执行函数,而节流则是在一定时间内只执行一次函数。
  2. 应用场景不同:防抖更适合处理一些需要用户稳定操作完成后的行为,如提交表单,而节流更适合处理高频率触发的事件,如鼠标移动、页面滚动等

拷贝

  • 深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的
和原数据是否指向同一对象第一层数据为基本类型原数据中含子对象
赋值新旧数据相互影响新旧数据相互影响
浅拷贝新旧数据互不影响新旧数据相互影响
深拷贝新旧数据互不影响新旧数据互不影响

赋值

赋值没有创建新对象,仅仅是拷贝了原对象的指针

赋值赋的其实是该对象的在栈中的地址,而不是堆中的数据、即两个对象指向的是同一块内存地址,无论哪个对象发生改变,其实都是改变的存空间的内容,因此,两个对象是联动的


let obj1 = {
    a: 1,
    b: 2,
    c: {
      q: 6
    }
  }

  let obj2 = obj1//因为obj2只是复制了obj1的引用,指向的是同一块内存地址,所以是联动的
  obj2.a = 3
  obj1.c.q = 7

  console.log(obj1) // {a: 3, b: 2, c: {q:7}}
  console.log(obj2) // {a: 3, b: 2, c: {q:7}}
  console.log(obj1===obj2) // true
  

浅拷贝

浅拷贝是创建一个新对象,这个对象仅对原对象的属性进行拷贝,属性值是基本类型时,拷贝的是原数据,属性值是引用类型时,拷贝的是指针。因此,如果原对象的属性有引用类型数据,无论修改新对象还是原对象的引用类型数据,另一个都会随之改变

  • 和赋值区别:
    • 赋值赋的其实是该对象的在栈中的地址,而不是堆中的数据。即两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,所以两个对象是联动的

    • 而浅拷贝是创建一个新对象,若属性是基本类型,拷贝的就是基本类型的值,若属性是引用类型,拷贝的就是内存地址 ,所以其中一个对象改变了这个地址会影响到另一个对象基本类型不影响,引用类型相互影响

    • Object.assign():把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象:let obj2 = Object.assign({}, obj1)
    • 展开运算符…与 Object.assign ()的功能相同:let obj2= {... obj1}
    • Array.prototype.concat()
    • Array.prototype.slice()返回一个新的副本对象
Object.assign()
  • 把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象,拷贝的是对象的属性的引用,而不是对象本身
var obj = { a: {a: "kobe", b: 39} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); //wade、新旧数据相互影响
console.log(obj===initalObj)//false
  • 当object只有一层的时候,是深拷贝
let obj = {username: 'kobe'};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}//新旧数据互不影响
console.log(obj === obj2) //false
Array.prototype.concat()
let arr = [1, 3, {username: 'kobe',age: 6}];
let arr2 = arr.concat();   
arr2[2].age = 7;
arr2[0] = 4
arr2[2].username = 'wade';
console.log(arr);//修改新对象会改到原对象,但数组里的第一层的基本类型新旧互不影响

//arr:[1, 3, {username: 'wade',age: 7}]
//arr2:[4, 3, {username: 'wade',age: 7}]
Array.prototype.slice()
let arr = [1, 3, {username: 'kobe',age: 6}];
let arr2 = arr.slice();
arr2[2].username = 'wade'
console.log(arr);//修改新对象会改到原对象,但数组里的第一层的基本类型新旧互不影响
//arr:[1, 3, {username: 'wade',age: 7}]
//arr2:[4, 3, {username: 'wade',age: 7}]

关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组

原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组
扩展运算符...
slice
let arr1 = [1, 2, { username: 'Kobe' }];
let arr2 = arr1.slice();

arr2[2].username = 'Wade'
arr1[0] = 3

console.log(arr1); // [3, 2, {username: 'Wade' }]
console.log(arr2); //[1, 2, {username: 'Wade' }]
手写实现
function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}

let obj1 = {...}
let obj2 = shallowClone(obj1)

深拷贝

深拷贝也是创建一个新对象,但不仅对原对象的属性进行拷贝,还在堆内存中开辟一个新的地址用来存储新对象,所以新对象和原对象没有关联,修改其中一个另一个不会变化

  • 无论是基本类型还是引用类型,拷贝的是一个新的完整的对象
  • 使用场景:
    • 原对象仅使用一次(被修改也不产生副作用),用赋值。
    • 原对象需多次使用(被修改会导致后续使用数据不准确),且属性只有基本类型,无引用类型,用浅拷贝。
    • 原数据需多次使用(被修改会导致后续使用数据不准确),且属性含有引用类型,用深拷贝
JSON.parse(JSON.stringify())
  • 原理:用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝
  • 第一层基本类型和后面的引用类型都互不影响
  • 但不能处理函数,因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数,会被转成null
let arr = [1, 3, {
    username: 'kobe',
    age: 6
}];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].username = 'wade';
arr2[2].age = 7
arr2[0] = 4
console.log(arr);
console.log(arr2)
//arr:[1, 3, {username: 'kobe',age: 6}]
//arr2:[4, 3, {username: 'wade',age: 7}]
递归
  • Date,正则,map,set等处理不完善
let arr = [1, 3, {
    username: 'kobe',
    age: 6
}];

const deepCopy = (obj) => {
    if (typeof obj !== 'object' || obj === null) return obj//判断数据不是对象,数组或null时直接返回该值
    const newObj = Array.isArray(obj) ? [] : {}
    for (const key in obj) {
        console.log(key); //username、age
        if (obj.hasOwnProperty(key)) {//拷贝是拷贝对象的自身属性,所以要规避掉原型上的自定义属性、自身属性(true),还是继承属性(false),
            newObj[key] = deepCopy(obj[key])
        }
    }
    return newObj
}

let obj2 = deepCopy(obj);
obj2[2].username = 'wade';
obj2[2].age = 7
obj2[0] = 4
console.log(obj);
console.log(obj2)
//obj:[1, 3, {username: 'kobe',age: 6}]
//obj2:[4, 3, {username: 'wade',age: 7}]

数组的操作

扁平化数组

递归

通过for循环,逐层逐个元素的去展平,若当前元素是一个数组,那就把它进行递归处理,再将递归处理的结果拼接到结果数组上

const flatten = (arr) => {
    let result = []
    let len = arr.length
    for (let i = 0; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))//递归展平结果拼接到结果数组上
        } else {
            result.push(arr[i])//否则直接加入结果数组
        }
    }
    return result
}
console.log(flatten(arr))[1, 2, 3, 4, 5, 6]
扩展运算符...+some
let arr = [1, [2, [3, 4, [5, 6]]]]
const flatten = (arr) => {
    while (arr.some(i => Array.isArray(i))) {
        arr = [].concat(...arr)
    }
    return arr

}
console.log(flatten(arr))[1, 2, 3, 4, 5, 6]
toString + split
  1. toString会将数组转成一个元素间以逗号相隔的字符串,它内部会先将数组展平一维后再转成字符串,因此可用toString进行展平
  2. 在通过split逗号分隔每个元素来复原一个包含所有元素的数组
  • 缺点:对于包含引用类型元素的数组来说,在toString过程中会发生类型转换,从而转换结果异常
let arr = [1, [2, [3, 4, [5, 6]]]]
const flatten = (arr) => {
    return arr.toString().split(',').map(i => Number(i))
}
console.log(flatten(arr))[1, 2, 3, 4, 5, 6]
Array.property.flat
let arr = [1, [2, [3, 4, [5, 6]]]]
const flatten = (arr) => {
    return arr.flat(Infinity)
}
console.log(flatten(arr))[1, 2, 3, 4, 5, 6]
reduce + 递归
let arr = [1, [2, [3, 4, [5, 6]]]]
const flatten = (arr) => {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
    }, [])
}
console.log(flatten(arr))[1, 2, 3, 4, 5, 6]

数组和对象相互转换

检测数组:判断一个对象是不是数组

  1. arr instanceof Array //true/false
  2. Object.prototype.toString.call(arr) == '[object Array]'
  3. Array.isArray(arr)
  4. arr.constructor == Array
  • Array.isArray 实现

 Array.myIsArray = function(arr) {
      return Object.prototype.toString.call(arr) === '[object Array]';
    };
    console.log(Array.myIsArray([])); // true
对象转数组

const obj = {a: 1,b: 2}

Object.entries(obj)
const arr = Object.entries(obj)//[['a':1],['b':2]]
Object.keys(obj)
const arr = Object.keys(obj).map(item => [item, obj[item]])
其中Object.keys(obj)//['a', 'b']
//[['a':1],['b':2]]
Object.values(obj)
const arr = Object.values(obj)//[1,2]
Array.from(array,fn,this)

从一个类似数组对象或可迭代对象创建一新的。浅拷贝的数组实例,返回一个新的数组实例

  const obj = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 2 //含有length和索引的对象,决定了返回数组的长度
}
const arr = Array.from(obj) //['name', 'age']

数组转对象

扩展运算符...
const arr = ['one', 'two']
const obj = {...arr}
console.log(obj)//{0: 'one', 1: 'two'}
forEach
let obj = {}
arr.forEach((item, index) => {
    obj[index] = item
})
console.log(obj) //{0: 'one', 1: 'two'}
Object.fromEntries()

把键值对转换为一个对象,返回由该迭代对象条目提供对应属性的新对象

 const arr = [    ['a', 1],
    ['b', 2]
]
const obj = Object.fromEntries(arr)
console.log(obj) //{a: 1, b: 2}
map/forEach
 let arr = [{
        id: 1,
        emNo: 18,
        name: '王'
    },
    {
        id: 2,
        emNo: 19,
        name: '仲'
    },
]
let obj = {}

arr.map(item => {
    obj[item.emNo] = item.name
})
console.log(obj) //{18: '王', 19: '仲'}

数组去重

let arr = [1,1,'true','true',true,true,false,false,undefined,undefined,null,null,NaN,NaN,'NaN','NaN','a','a',{},{},[1,2],[1,2],[],[],{a:1},{a:1}]

set
  • 类似数组,其成员的值都是唯一的,利用Array.from将set结构转成数组
  • 缺点:对象,数组未去重
const result = Array.from(new Set(arr))//对象,数组未去重
双重for循环+splice
  • 外层循环元素,内层循环时比较值,值相同时则删去这个值,否则push进数组
  • 缺点:NaN,对象,数组未去重
  function unique(arr) {
    let len = arr.length
    for (let i = 0; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1)
                len--
                j--
            }
        }
    }
    return arr
}
unique(arr)
indexOf
  • 缺点:NaN,对象,数组未去重
  • 新建一个空数组,遍历需要去重的数组,将数组元素存放到新数组中,存放前判断数组返回是否包含当前元素,没有则存入
  • 该方法返回调用他的string对象中第一次出现指定值的索引,从formIndex处进行搜索,若未找到该值,返回-1
  function unique(arr) {
      if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    const newArr = []
    arr.forEach(item => {
        if (newArr.indexOf(item) === -1) {
            newArr.push(item)
        }
    });
    return newArr
}
unique(arr)
includes
  • 和indexof方法异曲同工,只是用includes用来判断一个数组是否包含指定的值,根据情况若包含则返true,否则false
  • 缺点:对象,数组未去重
 function unique(arr) {
    const newArr = []
    arr.forEach(item => {
        if (!newArr.includes(item)) {//检测某个值是否存在
            newArr.push(item)
        }
    });

    return newArr
}
    unique(arr) 

hasOwnProperty
  • 缺点:空对象,数组都可去重,但有值的对象被删除了
 function unique(arr) {
    let obj = {}
    return arr.filter((item) => {
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
        //利用一个空的object对象,把数组的值存成objectkey值,如objcet[value] = true,在判断另一个值时,若object[value]存在的话说明该值重复
        //obj[typeof item + item]= true未直接用obj[item]是因123'123'不同,直接用前面的方法会判断为同一个值,因对象的键值只能是字符串,所以用typeof item+item拼成字符串作为key值来避免着恶搞问题
    })
}
unique(arr)
递归
  • 先排序,然后从最后开始比较,遇到相同则删除
  • 缺点: NaN,对象,数组未去重
 function unique(arr) {
    let array = arr;
    let len = array.length
    array.sort((a, b) => a - b) //排序后更加方便去重
    function loop(index) {
        if (index >= 1) {
            if (array[index] === array[index - 1]) {
                array.splice(index, 1)
            }
            loop(index - 1)//递归loop然后数组去重
        }
    }
    loop(len - 1)
    return array
}
unique(arr)
reduce+includes
function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]
filter+indexof
  • filter会创建一个新数组,对满足条件的元素存到一个新数组中,结合indexof进行判断
  • filter()把传入的函数依次作用于每个元素,然后根据返回值是true/false决定保留还是丢弃元素
const unique = arrs => {
    return arrs.filter((ele, index, array) => {
     //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
        return array.indexOf(ele,0)  === index
    })
}
unique(arrs);

push和pop实现

//push首推:底层实现是依赖于 数组的length属性的
Array.prototype.push2 = function(...rest){
  this.splice(this.length, 0, ...rest)
  return this.length;
}
//pop尾删--也是同理:
Array.prototype.pop2 = function(){
  return this.splice(this.length - 1, 1)[0];
}、

数组的深拷贝

const arr = [1,2,3,4]

  • 以下方法都返回一个新数组,新旧数据互不影响
slice

const newArr = arr.slice(arr)

concat

const newArr = arr.concat()

map

const newArr = arr.map(num => num)

Array.form

const newArr = Array.from(new Set(arr))

扩展运算符...

const newArr = [...arr]

forEach +push
const newArr = []
arr.forEach(val => {
    newArr.push(val)
})
for+push
const newArr = []
const len = arr.length
for (let i = 0; i < len; i++) {
    newArr.push(arr[i])
}

判断是否为数组

  • 最优:Array.isArray:Array.isArray(arr)

image.png

二分查找

  • 数组必须有序,不存在重复
  1. 每次比较数组中间的元素,若中间元素正好是要查找元素,则查找成功
  2. 否则根据中间元素与要查找的元素的大小关系确定新的查找范围、、或者每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,知道找到目标元素
 const BinarySearch = (arr, target) => {
    //arr 待排序数组、target 目标数据 
    let len = arr.length
    if (len <= 1) return -1
    let left = 0 //左边界
    let right = len - 1 //右边界

    while (left <= right) {
        const mid = Math.floor((left + right) / 2) //计算中间位置
        if (target > arr[mid]) {
            left = mid + 1 //则将左边界设为中间位置的右侧
        } else if (target < arr[mid]) {
            right = mid - 1 //否则将右边界设为中间位置的左侧
        } else {
            return mid
        }
    }
    return -1 //若没有找到目标元素则返回-1
}
const arr = [1, 4, 5, 6, 7, 8, 10]
console.log(BinarySearch(arr, 5)) //表示5在arr中的位置下标->2
console.log(BinarySearch(arr, 1)) //0

查找第一个值等于target

  • 数组必须有序,存在重复
  const BinarySearch = (arr, target) => {
    //arr 待排序数组、target 目标数据 
    let len = arr.length
    if (len <= 1) return -1
    let left = 0 //左边界
    let right = len - 1 //右边界

    while (left <= right) {
        const mid = Math.floor((left + right) / 2) //计算中间位置
        if (target > arr[mid]) {
            left = mid + 1 //则将左边界设为中间位置的右侧
        } else if (target < arr[mid]) {
            right = mid - 1 //否则将右边界设为中间位置的左侧
        } else {
            if (mid === 0 || arr[mid - 1] < target) return mid
            // 当 target=== arr[mid]时、如果 mid为0或者前一个数比 target 小那么就找到了第一个等于给定值的元素,直接返回
            right = mid -1 //// 否则高位下标为中间下标减1,继续查找
        }
    }
    return -1 //若没有找到目标元素则返回-1
}
const arr = [1, 4, 5, 6, 11, 11]
console.log(BinarySearch(arr, 11)) //4

查找最后一个值等于给定值的元素

  • 有序数据集合中存在重复的数据
 const BinarySearch = (arr, target) => {
    //arr 待排序数组、target 目标数据 
    let len = arr.length
    if (len <= 1) return -1
    let left = 0 //左边界
    let right = len - 1 //右边界

    while (left <= right) {
        const mid = Math.floor((left + right) / 2) //计算中间位置
        if (target > arr[mid]) {
            left = mid + 1 //则将左边界设为中间位置的右侧
        } else if (target < arr[mid]) {
            right = mid - 1 //否则将右边界设为中间位置的左侧
        } else {
            if (mid === 0 || arr[mid + 1] !== target) return mid
            // 当 target === arr[mid]时,若mid为0或者后一个数不等于 target 那么就找到了最后一个等于给定值的元素,直接返回
            // 这里不能取a rr[mid + 1] > target 可能会存在边界问题
            left = mid + 1 // 否则低位下标为中间下标加1,继续查找
        }
    }
    return -1 //若没有找到目标元素则返回-1
}
const arr = [1, 4, 5, 6, 11, 11]
console.log(BinarySearch(arr, 11)) //5

查找第一个大于等于给定值的元素

  • 有序数据集合中存在重复的数据
 const BinarySearch = (arr, target) => {
    //arr 待排序数组、target 目标数据 
    let len = arr.length
    if (len <= 1) return -1
    let left = 0 //左边界
    let right = len - 1 //右边界

    while (left <= right) {
        const mid = Math.floor((left + right) / 2) //计算中间位置
        if (target <= arr[mid]) {
            // 如果 midIndex 为0或者前一个数小于 target 那么找到第一个大于等于给定值的元素,直接返回
            if (mid === 0 || arr[mid - 1] < target) return mid
            right = mid - 1// 否则高位下标为中位下标减1
        } else {
            left = mid + 1
        }

    }
    return -1 //若没有找到目标元素则返回-1
}
const arr = [1, 4, 5, 6, 7, 8, 11, 11, 11]
console.log(BinarySearch(arr, 8)) //5

查找最后一个小于等于给定值的元素

  • 有序数据集合中存在重复的数据
const BinarySearch = (arr, target) => {
    //arr 待排序数组、target 目标数据 
    let len = arr.length
    if (len <= 1) return -1
    let left = 0 //左边界
    let right = len - 1 //右边界

    while (left <= right) {
        const mid = Math.floor((left + right) / 2) //计算中间位置
        if (target >= arr[mid]) {
            // 如果 mid 最后一个或者后一个数大于 target 那么找到最后一个小于等于给定值的元素,直接返回
            if (mid === len - 1 || arr[mid + 1] > target) return mid
            left = mid + 1 //否则低位下标为中位下标加1
        } else {
            right = mid - 1
        }
    }
    return -1 //若没有找到目标元素则返回-1
    }
    const arr = [1, 4, 5, 6, 7, 8, 11, 11, 11]
    console.log(BinarySearch(arr, 4)) //1

找到第一个不重复字符的下标或字母

//第一种方法
let str = 'eertre'
const findOneStr = (str) => {
    if (!str) return -1
    let obj = {} //存储每个字符出现的次数
    let arr = str.split('')
    arr.forEach(item => {
        let val = obj[item]
        obj[item] = val ? val + 1 : 1 //val为undefined时表示未存储,obj[item] =1
    });
    let len = arr.length
    for (let i = 0; i < len; i++) {
        if (obj[arr[i]] == 1) return i //返回下标  return arr[i]是返回该字符
    }
    return -1
}
findOneStr(str)//3
//第二种方法
 let str = 'eettre'
const fun = (str) => {
    let arr = [];
    let arr1 = [];
    arr = str.split('')

    arr.forEach(item => {
        let num = str.split(item).length - 1//通过指定分割符合分割字符串
        if (num == 1) arr1.push(item)//当只有一个时,切割后的数组长度为2,num会为1,把该字符push到arr1中
    });

    if (arr1.length !== 0) return arr1[0]//返回该字符串
    return '无符合条件的值'

}
console.log(fun(str))//r

比较2个版本号的大小

  • 若v1大于v2返回1、v1小于v2返回-1,否则返回0
  • 将版本号转成数组,两两比较
 const compareVersion = (v1, v2) => {
    if (v1 == v2) return 0
    const vs1 = v1.split(".").map(e => parseInt(e));//使字符串转成数字
    const vs2 = v2.split(".").map(e => parseInt(e));

    const length = Math.max(vs1.length, vs2.length);//取最大长风值

    for (let i = 0; i < length; i++) {
        if (vs1[i] > vs2[i]) return 1;
        if (vs1[i] < vs2[i]) return -1
    }

    if (length == vs1.length) return -1;
    return 1;
}

console.log(compareVersion('2.2.3', '2.2.2'))//1

版本号排序

const verList = ['0.1.1', '2.3.3', '0.3002.1', '4.2', '4.3.5', '4.3.4.5', '4.3.5.1']
 const sort = (ver) => {
    return ver.sort((v1, v2) => compareVersion(v1, v2));//compareVersion是比较两个版本号大小的方法
}
//['0.1.1', '0.3002.1', '2.3.3', '4.2', '4.3.4.5', '4.3.5.1', '4.3.5']

Object.create基本实现原理

//Object.create()实现原理、将传入的对象作为创建的对象的原型
function create(obj) {
  function F() {}
  F.prototype = obj // 将被继承的对象作为空函数的prototype

// 返回new期间创建的新对象,此对象的原型为被继承的对象, 通过原型链查找可以拿到被继承对象的属性
  return new F() 
}

reverse底层原理和扩展

// 用于颠倒*数组*中元素的顺序,会改变原数组
Array.prototype.myReverse = function () {
    var len = this.length;
    for (var i = 0; i < len; i++) {
        var temp = this[i];
        this[i] = this[len - 1 - i];
        this[len - 1 - i] = temp;
    }
    return this;
}

new的原理

  1. 创建一个新的空对象(类型时object)
  2. 空对象的内部属性__proto__赋值为构造函数的prototype属性
  3. 将构造函数的this指向新对象
  4. 执行构造函数内部代码,通过this给新对象添加属性方法
  5. 返回新对象(若构造函数内部通过return返回一个引用类型,则new操作最终返回这个引用类型值,否则返回刚创建的新对象)
//第一种方式
 function myNew(func, ...args) {
    let obj = {}
    obj.__proto__ = func.prototype
    let res = func.apply(obj, args)
    return res instanceof Object ? res : obj
}
//第二种方式
 function createNew() {
    let newObject = null
    let constructor = Array.prototype.shift.call(arguments)
    let result = null
    if (typeof constructor !== 'function') return //判断参数是不是函数
    newObject = Object.create(constructor.prototype)
    result = constructor.apply(newObject, arguments)
    let flag = result && (typeof result === 'object' || typeof result === 'function')
    return flag ? result : newObject
}
createNew('构造函数,初始化函数')

回文数

字符串的转换

先将数字转成字符串,再经过变成数组,数组反转,数组变成字符串之后,最后比较两组字符串

let num = 123210
const isPalindrome = (x) => {
    if (x < 0) return false
    let str = '' + x
    return Array.from(str).reverse().join('') === str
}

数字转换:求末得尾数, 除10得整数

  1. 先判断一些特殊情况【 小于0的、 尾数为0的、 小于10的正整数】
  2. 之后, 将整数反转, 反转前后两个整数是否相等来判断是否为回文整数

这里的反转:将整数求末得到尾数,之后每求一次末,都再原数上添加一位(通过 * 10 来得到),就能得到一个反转的数

计算需要求末的次数: 将整数除10,来计算求模的次数,Math.floor() 返回小于或等于一个给定数字的最大整数

let num = 12321
const isPalindrome = (x) => {
    if (x < 0 || (x !== 0 && x % 10 === 0)) return false
    if (0 <= x && x < 10) return true

    let res = x
    let num = 0
    while (x !== 0) {
        num = x % 10 + num * 10
        x = Math.floor(x / 10)
    }
    return res === num
}
console.log(isPalindrome(num)) //true

最长回文串

let str = 'afrttrr'
const longStr = (str) => {
    if (str.length < 2) return str
    let res = ' '
    let len = str.length

    for (let i = 0; i < len; i++) {
        helper(i, i) //回文子串长度是奇数 aba
        helper(i, i + 1) //回文子串长度是偶数 abba

        function helper(m, n) {
            while (m >= 0 && n < len && str[m] == str[n]) {
                m--
                n++
            }
            //此处是m,n的值循环后,是恰好不满足循环条件的时刻
            //此时m到n的距离为n-m+1,但m,n两个边界不能取,所以应取m+1到n-1的区间,长度是n-m-1
            if (n - m - 1 > res.length) {
                res = str.slice(m + 1, n)//slice也要取(m+1,n-1]这个区间
            }
        }
    }
    return res

}
console.log(longStr(str))

继承方式

原型链继承prototype:父类用this声明的属性被所有实例共享

function Parent() {
    this.name = 'kevin';
    this.age = 18;
    this.getName = function () {
      console.log(this.name);
    }
  }

  Parent.prototype.getAge = function () {
    console.log(this.age);
  }

  function Child() {}

  Child.prototype = new Parent();

  var child1 = new Child();
  console.log(child1.getName(),child1.getAge()) // kevin,18
  
  child1.name = 'wsn'
  child1.age = 20
  console.log(child1.getName(), child1.getAge()) // wsn,20

优点:

  1. fatherFn通过this声明的属性/方法都会绑定在new期间创建的新对象上。
  2. 新对象的原型是father.prototype,通过原型链的属性查找到father.prototype的属性和方法

缺点:

  • 父类所有引用类型(对象,数组)会被子类共享,更改一个子类的数据,其他数据也受影响
  • 子类实例不能给父类参,不够灵活

原型式继承(Object.create())

继承对象原型-Object.create()实现

let oldObj = { p: 1 };
let newObj = cloneObject(oldObj)
oldObj.p = 2
console.log('oldObj newObj', oldObj, newObj)

优点: 兼容性好,最简单的对象继承。

缺点:

  • 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
  • 因为旧对象(oldObj)是实例对象(newObj)的原型,多个实例共享被继承对象的属性,存在篡改的可能。
  • 无法传参

借用构造函数继承(call)

function Parent (name) {
    this.age = 10
    this.name = name;
}

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

var child1 = new Child('kevin');

console.log(child1.name); // kevin

var child2 = new Child('daisy');

console.log(child2.name); // daisy




//第二种
function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr // 父类的参数
}
    
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';

function sonFn(fatherParams, ...sonParams) {
  fatherFn.call(this, ...fatherParams); // 核心步骤: 将fatherFn的this指向sonFn的this对象上
  this.obkoro1 = '子类的this属性';
  this.sonParams = sonParams; // 子类的参数
}

sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'

let fatherParamsArr = ['父类的参数1', '父类的参数2']

let sonParamsArr = ['子类的参数1', '子类的参数2']

const sonFnInstance = new sonFn(fatherParamsArr, ...sonParamsArr); // 实例化子类

console.log('借用构造函数子类实例', sonFnInstance)
  • 优点:
    • 父类的引用类型不被子类共享,不会互相影响
    • 子类可向父类传参
  • 缺点:
    • 子类只能继承父类通过this声明的属性和方法,不能继承父类prototype上的属性和方法
    • 方法都在构造函数中,每次创建实例都会创建一遍方法,会调用2次父类的构造函数,会有2份一样的的属性和方法,影响性能
    • 父类方法无法复用:因为无法继承父类的prototype,所以每次子类实例化都要执行父类函数,重新声明父类this里所定义的方法,因此方法无法复用

组合式继承(call+new)

原理:使用原型链继承(new)将this和prototype声明的属性/方法继承至子类的prototype上,使用借用构造函数来继承父类通过this声明属性和方法至子类实例的属性上

 function Parent() {
    this.age = 10
}

function child() {
    this.name = 'test'
    Parent.call(this)
}
child.prototype = new Parent()
let o1 = new child()
  • 优点:
    • 父类通过this声明的属性和方法被子类实例共享的问题(原型链继承中存在的问题)-
    • 父类通过prototype声明的属性和方法无法继承的问题(借用构造函数继承存的问题)
  • 缺点:
    • 2次调用父类函数造成一定性能消耗
    • 因调用两次父类,导致父类通过this声明的属性/方法,生成两份的问题。

es6继承

 class Parent {
    constructor() {
        this.age = 10
    }
}
class child extends Parent {
    constructor() {
        super();
        this.name = 'test'
    }
}

寄生组合式继承(call+寄生式继承(封装继承过程))

原理:

  1. 使用借用构造函数(call)来继承父类this声明的属性/方法
  2. 通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法
function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn() {
  fatherFn.call(this, '借用构造继承'); // 核心1 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上
  this.obkoro1 = '子类的this属性';
}
// 核心2 寄生式继承:封装了son.prototype对象原型式继承father.prototype的过程,并且增强了传入的对象。
function inheritPrototype(son, father) {
  const fatherFnPrototype = Object.create(father.prototype); // 原型式继承:浅拷贝father.prototype对象 father.prototype为新对象的原型
  son.prototype = fatherFnPrototype; // 设置father.prototype为son.prototype的原型
  son.prototype.constructor = son; // 修正constructor 指向
}
inheritPrototype(sonFn, fatherFn)
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
const sonFnInstance = new sonFn();
console.log('寄生组合式继承子类实例', sonFnInstance)

修正constructor指向de原因:

constructor的作用:返回创建实例对象的Object构造函数的引用即返回实例对象的构造函数的引用: let instance = new sonFn() instance.constructor // sonFn函数

constructor的应用场景: 1.当只有实例对象没有构造函数的引用时:某些场景下,对实例对象经过多轮导入导出,不知道实例是从哪个函数中构造出来或者追踪实例的构造函数,较为艰难 let instance = new sonFn() // 实例化子类 export instance; // 多轮导入+导出,导致sonFn追踪非常麻烦,或者不想在文件中再引入sonFn let fn = instance.constructor 2.保持constructor指向的一致性:因此每次重写函数的prototype都应该修正一下constructor的指向,以保持读取constructor行为的一致性

寄生组合式继承是最成熟的继承方法, 也是现在最常用的继承方法,众多JS库采用的继承方案也是它。

优点:

  1. 只调用一次父类fatherFn构造函数。
  2. 避免在子类prototype上创建不必要多余的属性。
  3. 使用原型式继承父类的prototype,保持了原型链上下文不变。 子类的prototype只有子类通过prototype声明的属性/方法和父类prototype上的属性/方法泾渭分明

ES5继承与ES6继承的区别:

  1. ES5的继承实质上是先创建子类的实例对象,再将父类的方法添加到this上。
  2. ES6的继承是先创建父类的实例对象this,再用子类的构造函数修改this。 因为子类没有自己的this对象,所以必须先调用父类的super()方法

水平垂直居中:已知宽高的情况

文本对齐:text-align:center;line-hight:height

flex

  • 只设置父元素:display:flex;justify-content:center;align-self:center;
  • 父子元素都设置:父:display:flex;子:align-self:center;margin:auto

position子绝父相 + margin

  • 父:position:relative
  • 子position:absolute;top/bottom/left/right各为0;margin:auto

position子绝父相 + 负margin

  • 父:flex:relative
  • 子position:absolute;top/left各50%;margin-top:1/2子高;margin-left:1/2子高

position子绝父相 + transform

  • 父position:relative
  • 子position:absolute;top/left各50%; transform:translate(-50%,-50%)

position+calc函数(宽高各为200px)

  • 父position:relative
  • 子absolute;top:calc(50%,-100px);left:calc(50%,-100px)

两栏布局:左固定,右自适应

 <div class="outer1">
    <div class="left">left</div>
    <div class="right">right</div>
</div>

flex

  • 父:display:flex
  • 右:flex:1(占比可自动铺满剩余空间)

position

  • 第一种
    • 父:flex:relative
    • 右:float:absolute;top/right各为;left:左宽
.outer1 {
    position: relative;
}
.left {
    width: 200px;
    background-color: red;
}
.right {
    background-color: yellow;
    position: absolute;
    top: 0;
    left: 200px;
    right: 0;
}
  • 第二种
    • 父:flex:relative;overflow:hidden
    • 左(脱离文档流):flex:absolute;left/right各为0
    • 右:margin-left:左宽
 .outer1 {
   position: relative;
}
.left {
   position: absolute;
   width: 200px;
}
.right {
   margin-left: 200px;
}

float+overflow

  • 左:float:left
  • 右:overflow:hidden

float + margin

  • 左:float:left
  • 右:margin-left:左宽

三栏布局:两侧固定,中间自适应

<div class="outer outer1">
   <div class="left">1-left</div>
   <div class="middle">1-middle</div>
   <div class="right">1-right</div>
</div>

flex

div顺序:left、center、right,左右均设宽高,中间为了自适应设高不设宽

  • 父display:flex
  • 中flex:1
 .outer1 {
    display: flex;
    text-align: center;
}
.left {
    flex: 0 0 100px;
}
.middle {
    flex: auto;
}
.right {
    flex: 0 0 200px;
}

position:左右absolute

div顺序:left、center、right,左右均设宽高,中间为了自适应设高不设宽

  • 左position:absolute;top/left各为0
  • 中position:absolute;top/right各为0
  • 右margin-left:左宽;margin-right:右宽

float

 <div class="outer1">
    <div class="left">1-left</div>
    <div class="right">1-right</div>
    <div class="middle">1-middle</div>
</div>
  • 左float:left
  • 中margin:0 右宽 0 左宽
  • 右float:right
.outer1 {
    width: 100%;
    height: 100px;
    text-align: center;
}
.left {
    float: left;
    width: 200px;
}
.middle {
    margin: 0 200px;
}
.right {
    width: 200px;
    float: right;
}

圣杯布局&双飞翼布局

  • 相同之处:
  1. 两侧内容宽度固定,中间内容宽度自适应
  2. 三栏布局,中间一栏最先加载、渲染出来
  3. 都使用了float浮动向左脱离文档流,让左中右三列浮动,通过父外边距形成三列布局
  • 不同之处:

1、实现方法的不同

  • 圣杯布局:通过float搭建布局+margin使三列布局到一行上+relative相对定位调整位置。
  • 双飞翼布局:通过float+margin没有使用相对定位

2、怎么处理两列的位置:

  • 圣杯布局:给外部容器加padding,通过相对定位把两边定位出来。
  • 双飞翼布局:是靠在中间这层外面套一层divpadding将内容挤出来中间。

圣杯布局:relative+float

  • div顺序:center,left,right
<div id="container">
<div id="center" class="column">#center</div>
<div id="left" class="column">#left</div>
<div id="right" class="column">#right</div>
</div>

#container {
    /* 因左右侧会遮住center,给外层的container设置 padding-left: 200px;padding-right: 150px;,
    给left和right空出位置 */
    padding-left: 200px;
    padding-right: 150px;
    overflow: hidden;
}

#container .column {
    position: relative;
    float: left;
    text-align: center;
    height: 300px;
    line-height: 300px;
}

#center {
    width: 100%;  /*这样因为浮动的关系,center会占据整个container,左右两块区域被挤下去了 */
    background: rgb(206, 201, 201);
}

#left {
    width: 200px;
    right: 200px;
    margin-left: -100%;/* 让left回到上一行最左侧 */
    background: rgba(95, 179, 235, 0.972);
}

#right {
    width: 150px;
    margin-left: -150px; /* 把right拉回第一行,这时右侧空出了150px的空间,所以最后设置 right: -150px;把right区域拉到最右侧就行了 */
    right: -150px;
    background: rgb(231, 105, 2);
}

双飞翼布局

  1. 两侧内容宽度固定,中间内容宽度自适应
  2. 三栏布局,中间一栏最先加载、渲染出来 实现:
  • left、center、right三种都设置左浮动
  • 设置center宽度为100%
  • 设置负边距,left设置负边距为100%,right设置负边距为自身宽度
  • 设置content的margin值为左右两个侧栏留出空间,margin值大小为left和right宽度
  <div id="container">
    <div id="center" class="column">
      <div class="content">#center</div>
    </div>
    <div id="left" class="column">#left</div>
    <div id="right" class="column">#right</div>
  </div>
  
   #container {
    overflow: hidden;
  }

  .column {
    text-align: center;
    height: 300px;
    line-height: 300px;
  }

  #left,
  #right,
  #center {
    float: left;
  }

  #center {
    width: 100%;
    background: rgb(206, 201, 201);
  }

  #left {
    width: 200px;
    margin-left: -100%;
    background: rgba(95, 179, 235, 0.972);
  }

  #right {
    width: 150px;
    margin-left: -150px;
    background: rgb(231, 105, 2);
  }

  .content {
    margin: 0 150px 0 200px;
  }

九宫格

  <div class="container">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
</div>

浮动+百分比

 .container {
    width: 300px;
    height: 300px;
}
.item {
    float: left;
    width: 33.33%;
    height: 33.33%;
    box-sizing: border-box;
    font-size: 32px;
    text-align: center;
    border: 1px solid #e5e4e9;
}

flex布局

 .container {
    display: flex;
    flex-wrap: wrap;
    width: 150px;
    height: 150px;
}
.item {
    width: 33.33%;
    height: 33.33%;
    box-sizing: border-box;
    font-size: 2em;
    text-align: center;
    border: 1px solid #e5e4e9;
}