• 来自2年前端的面经


    实现 JSONP 跨域

    JSONP 核心原理script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;

    实现

    const jsonp = (url, params, callbackName) => {
        const generateUrl = () => {
            let dataSrc = "";
            for(let key in params) {
                if(params.hasOwnProperty(key)) {
                    dataSrc += `${key}=${params[key]}&`
                }
            }
            dataSrc += `callback=${callbackName}`;
            return `${url}?${dataSrc}`;
        }
        return new Promise((resolve, reject) => {
            const scriptEle = document.createElement('script');
            scriptEle.src = generateUrl();
            document.body.appendChild(scriptEle);
            window[callbackName] = data => {
                resolve(data);
                document.removeChild(scriptEle);
            }
        });
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    计算属性和watch有什么区别?以及它们的运用场景?

    // 区别
      computed 计算属性:依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值。
      watch 侦听器:更多的是观察的作用,无缓存性,类似与某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作
    //运用场景
      当需要进行数值计算,并且依赖与其它数据时,应该使用computed,因为可以利用computed的缓存属性,避免每次获取值时都要重新计算。
      当需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch选项允许执行异步操作(访问一个API),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    代码输出结果

    function SuperType(){
        this.property = true;
    }
    
    SuperType.prototype.getSuperValue = function(){
        return this.property;
    };
    
    function SubType(){
        this.subproperty = false;
    }
    
    SubType.prototype = new SuperType();
    SubType.prototype.getSubValue = function (){
        return this.subproperty;
    };
    
    var instance = new SubType();
    console.log(instance.getSuperValue());
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果:true

    实际上,这段代码就是在实现原型链继承,SubType继承了SuperType,本质是重写了SubType的原型对象,代之以一个新类型的实例。SubType的原型被重写了,所以instance.constructor指向的是SuperType。具体如下:

    instanceof

    作用:判断对象的具体类型。可以区别 arrayobjectnullobject 等。

    语法A instanceof B

    如何判断的?: 如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false

    注意:如果检测原始值,则始终返回 false

    实现:

    function myinstanceof(left, right) {
        // 基本数据类型都返回 false,注意 typeof 函数 返回"function"
        if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
        let leftPro = left.__proto__;  // 取左边的(隐式)原型 __proto__
        // left.__proto__ 等价于 Object.getPrototypeOf(left)
        while(true) {
            // 判断是否到原型链顶端
            if(leftPro === null) return false;
            // 判断右边的显式原型 prototype 对象是否在左边的原型链上
            if(leftPro === right.prototype) return true;
            // 原型链查找
            leftPro = leftPro.__proto__;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    参考:前端进阶面试题详细解答

    动态规划求解硬币找零问题

    题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

    示例1输入: coins = [1, 2, 5], amount = 11
    输出: 3
    解释: 11 = 5 + 5 + 1
    
    示例2输入: coins = [2], amount = 3
    输出: -1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实现代码如下:

    const coinChange = function (coins, amount) {
      // 用于保存每个目标总额对应的最小硬币个数
      const f = [];
      // 提前定义已知情况
      f[0] = 0;
      // 遍历 [1, amount] 这个区间的硬币总额
      for (let i = 1; i <= amount; i++) {
        // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新
        f[i] = Infinity;
        // 循环遍历每个可用硬币的面额
        for (let j = 0; j < coins.length; j++) {
          // 若硬币面额小于目标总额,则问题成立
          if (i - coins[j] >= 0) {
            // 状态转移方程
            f[i] = Math.min(f[i], f[i - coins[j]] + 1);
          }
        }
      }
      // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1
      if (f[amount] === Infinity) {
        return -1;
      }
      // 若有解,直接返回解的内容
      return f[amount];
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    代码输出结果

    var myObject = {
        foo: "bar",
        func: function() {
            var self = this;
            console.log(this.foo);  
            console.log(self.foo);  
            (function() {
                console.log(this.foo);  
                console.log(self.foo);  
            }());
        }
    };
    myObject.func();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出结果:bar bar undefined bar

    解析:

    1. 首先func是由myObject调用的,this指向myObject。又因为var self = this;所以self指向myObject。
    2. 这个立即执行匿名函数表达式是由window调用的,this指向window 。立即执行匿名函数的作用域处于myObject.func的作用域中,在这个作用域找不到self变量,沿着作用域链向上查找self变量,找到了指向 myObject对象的self。

    说一下HTTP和HTTPS协议的区别?

    1HTTPS协议需要CA证书,费用较高;HTTP协议不需要
    2HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;
    3、使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;
    4HTTP协议连接很简单,是无状态的;HTTPS协议是具有SSLHTTP协议构建的可进行加密传输、身份认证的网络协议,HTTP更加安全
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码输出结果

    function foo(something){
        this.a = something
    }
    
    var obj1 = {
        foo: foo
    }
    
    var obj2 = {}
    
    obj1.foo(2); 
    console.log(obj1.a); // 2
    
    obj1.foo.call(obj2, 3);
    console.log(obj2.a); // 3
    
    var bar = new obj1.foo(4)
    console.log(obj1.a); // 2
    console.log(bar.a); // 4
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果: 2 3 2 4

    解析:

    1. 首先执行obj1.foo(2); 会在obj中添加a属性,其值为2。之后执行obj1.a,a是右obj1调用的,所以this指向obj,打印出2;
    2. 执行 obj1.foo.call(obj2, 3) 时,会将foo的this指向obj2,后面就和上面一样了,所以会打印出3;
    3. obj1.a会打印出2;
    4. 最后就是考察this绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出4。

    setTimeout 模拟 setInterval

    描述:使用setTimeout模拟实现setInterval的功能。

    实现

    const mySetInterval(fn, time) {
        let timer = null;
        const interval = () => {
            timer = setTimeout(() => {
                fn();  // time 时间之后会执行真正的函数fn
                interval();  // 同时再次调用interval本身
            }, time)
        }
        interval();  // 开始执行
        // 返回用于关闭定时器的函数
        return () => clearTimeout(timer);
    }
    
    // 测试
    const cancel = mySetInterval(() => console.log(1), 400);
    setTimeout(() => {
        cancel();
    }, 1000);  
    // 打印两次1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    代码输出结果

    function a(xx){
      this.x = xx;
      return this
    };
    var x = a(5);
    var y = a(6);
    
    console.log(x.x)  // undefined
    console.log(y.x)  // 6
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出结果: undefined 6

    解析:

    1. 最关键的就是var x = a(5),函数a是在全局作用域调用,所以函数内部的this指向window对象。**所以 this.x = 5 就相当于:window.x = 5。**之后 return this,也就是说 var x = a(5) 中的x变量的值是window,这里的x将函数内部的x的值覆盖了。然后执行console.log(x.x), 也就是console.log(window.x),而window对象中没有x属性,所以会输出undefined。
    2. 当指向y.x时,会给全局变量中的x赋值为6,所以会打印出6。

    代码输出结果

    const promise1 = new Promise((resolve, reject) => {
      console.log('promise1')
      resolve('resolve1')
    })
    const promise2 = promise1.then(res => {
      console.log(res)
    })
    console.log('1', promise1);
    console.log('2', promise2);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出结果如下:

    promise1
    1 Promise{<resolved>: resolve1}
    2 Promise{<pending>}
    resolve1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    需要注意的是,直接打印promise1,会打印出它的状态值和参数。

    代码执行过程如下:

    1. script是一个宏任务,按照顺序执行这些代码;
    2. 首先进入Promise,执行该构造函数中的代码,打印promise1
    3. 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来;
    4. 碰到promise1.then这个微任务,将它放入微任务队列;
    5. promise2是一个新的状态为pendingPromise
    6. 执行同步代码1, 同时打印出promise1的状态是resolved
    7. 执行同步代码2,同时打印出promise2的状态是pending
    8. 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

    代码输出结果

    setTimeout(function () {
      console.log(1);
    }, 100);
    
    new Promise(function (resolve) {
      console.log(2);
      resolve();
      console.log(3);
    }).then(function () {
      console.log(4);
      new Promise((resove, reject) => {
        console.log(5);
        setTimeout(() =>  {
          console.log(6);
        }, 10);
      })
    });
    console.log(7);
    console.log(8);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果为:

    2
    3
    7
    8
    4
    5
    6
    1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    代码执行过程如下:

    1. 首先遇到定时器,将其加入到宏任务队列;
    2. 遇到Promise,首先执行里面的同步代码,打印出2,遇到resolve,将其加入到微任务队列,执行后面同步代码,打印出3;
    3. 继续执行script中的代码,打印出7和8,至此第一轮代码执行完成;
    4. 执行微任务队列中的代码,首先打印出4,如遇到Promise,执行其中的同步代码,打印出5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;
    5. 执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为100ms,第二个定时器的时间为10ms,所以先执行第二个定时器,打印出6;
    6. 此时微任务队列为空,继续执行宏任务队列,打印出1。

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

    你在工作终于到那些问题,解决方法是什么

    经常遇到的问题就是Cannot read property ‘prototype’ of undefined
    解决办法通过浏览器报错提示代码定位问题,解决问题
    
    Vue项目中遇到视图不更新,方法不执行,埋点不触发等问题
    一般解决方案查看浏览器报错,查看代码运行到那个阶段未之行结束,阅读源码以及相关文档等
    然后举出来最近开发的项目中遇到的算是两个比较大的问题。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    说一下怎么把类数组转换为数组?

    //通过call调用数组的slice方法来实现转换
    Array.prototype.slice.call(arrayLike)
    
    //通过call调用数组的splice方法来实现转换
    Array.prototype.splice.call(arrayLike,0)
    
    //通过apply调用数组的concat方法来实现转换
    Array.prototype.concat.apply([],arrayLike)
    
    //通过Array.from方法来实现转换
    Array.from(arrayLike)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    节流与防抖

    • 函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
    • 函数节流 是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
    // 函数防抖的实现
    function debounce(fn, wait) {
      var timer = null;
    
      return function() {
        var context = this,
          args = arguments;
    
        // 如果此时存在定时器的话,则取消之前的定时器重新记时
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }
    
        // 设置定时器,使事件间隔指定事件后执行
        timer = setTimeout(() => {
          fn.apply(context, args);
        }, wait);
      };
    }
    
    // 函数节流的实现;
    function throttle(fn, delay) {
      var preTime = Date.now();
    
      return function() {
        var context = this,
          args = arguments,
          nowTime = Date.now();
    
        // 如果两次时间间隔超过了指定时间,则执行函数。
        if (nowTime - preTime >= delay) {
          preTime = Date.now();
          return fn.apply(context, args);
        }
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    对this对象的理解

    this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

    • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
    • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
    • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
    • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

    这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

    Object.is()

    描述Object.is 不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。

    1. NaN=== 中是不相等的,而在 Object.is 中是相等的
    2. +0-0=== 中是相等的,而在 Object.is 中是不相等的

    实现:利用 ===

    Object.is = function(x, y) {
        if(x === y) {
            // 当前情况下,只有一种情况是特殊的,即 +0 -0
            // 如果 x !== 0,则返回true
            // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
            return x !== 0 || 1 / x === 1 / y;
        }
        // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
        // x和y同时为NaN时,返回true
        return x !== x && y !== y;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    渐进增强和优雅降级之间的区别

    (1)渐进增强(progressive enhancement):主要是针对低版本的浏览器进行页面重构,保证基本的功能情况下,再针对高级浏览器进行效果、交互等方面的改进和追加功能,以达到更好的用户体验。 (2)优雅降级 graceful degradation: 一开始就构建完整的功能,然后再针对低版本的浏览器进行兼容。

    两者区别:

    • 优雅降级是从复杂的现状开始的,并试图减少用户体验的供给;而渐进增强是从一个非常基础的,能够起作用的版本开始的,并在此基础上不断扩充,以适应未来环境的需要;
    • 降级(功能衰竭)意味着往回看,而渐进增强则意味着往前看,同时保证其根基处于安全地带。

    “优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。 在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。

    “渐进增强”观点则认为应关注于内容本身。内容是建立网站的诱因,有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。

    TLS/SSL的工作原理

    TLS/SSL全称安全传输层协议(Transport Layer Security), 是介于TCP和HTTP之间的一层安全协议,不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造。

    TLS/SSL的功能实现主要依赖三类基本算法:散列函数hash对称加密非对称加密。这三类算法的作用如下:

    • 基于散列函数验证信息的完整性
    • 对称加密算法采用协商的秘钥对数据加密
    • 非对称加密实现身份认证和秘钥协商
    (1)散列函数hash

    常见的散列函数有MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。

    特点: 在信息传输过程中,散列函数不能三都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密。

    (2)对称加密

    对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。

    常见的对称加密算法有AES-CBC、DES、3DES、AES-GCM等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。

    特点: 对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

    (3)非对称加密

    非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。

    常见的非对称加密算法有RSA、ECC、DH等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。

    特点: 非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

    综合上述算法特点,TLS/SSL的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。

    说说浏览器缓存

    缓存可以减少网络 IO 消耗,提高访问速度。浏览器缓存是一种操作简单、效果显著的前端性能优化手段
    很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。
    但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
    
    Memory Cache
    Service Worker Cache
    HTTP Cache
    Push Cache
    
    缓存它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存
        实现强缓存,过去我们一直用 expires。    当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段,现在一般使用Cache-Control 两者同时出现使用Cache-Control         协商缓存,Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:每次请求去判断这个时间戳是否发生变化。    从而去决定你是304读取缓存还是给你返回最新的数据
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    Sqlserver限制账户在哪些ip下才可以访问数据库
    mysql 忘记 root 密码的解决办法(针对不同 mysql 版本)
    使用DQL命令查询数据
    ENVI_常用扩展工具名
    吃透MySQL(十四):主从延时问题
    c++ 关于bfs和dfs的相对统一写法
    简单介绍webmagic的使用
    系统编程07-线程的互斥锁、读写锁、条件变量
    make&Makefile
    SQL生成自然数,日历序列 浅析
  • 原文地址:https://blog.csdn.net/loveX001/article/details/127800538