Web前端各面试经总结笔记

即将参加面试,逛了几个论坛学习了很多前端大牛的面试经,学习了很多,现收集总结与大家分享共同学习。

初学者阅后也要用心钻研其中的原理,重要知识需要系统学习、透彻学习,形成自己的知识链。临时抱佛脚只求面试侥幸混过关是错误的!也是不可能的!

本文比较基础精炼,欢迎评论指正,文章会不断更新。

? 文章列表

 

一、JavaScript

 

1.如何理解闭包


    function A() {
      let a = 1;
      function B() {
          console.log(a);
      }
      return B;
    }
    //函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包
    //就是闭包

    // 根据作用域链的规则,底层作用域没有声明的变量,会向上一级找,找到就返回,没找到就一直找,直到window的变量,没有就返回undefined。
    var count = 10;

    var cc = 'sss';

    function add() {
        var count = 0;

        return function () {
            console.log(count++);
            console.log(this.count++);
            console.log(cc);
        }
    }

    var s = add();
    s(); //0 10 sss
    s(); //1 11 sss
    s(); //2 12 sss



    // 循环中的闭包
    for (var i = 0; i < 10; i++) {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    }
    // 上面的代码不会输出数字 0 到 9,而是会输出数字 10 十次。

    // 解决方法
    // 为了正确的获得循环序号,最好使用 匿名包装器,其实就是我们通常说的自执行匿名函数。
    for (var i = 0; i < 10; i++) {
        (function (e) {
            setTimeout(function () {
                console.log(e);
            }, 1000);
        })(i);
    }
    // 从匿名包装器中返回一个函数
    for (var i = 0; i < 10; i++) {
        setTimeout((function (e) {
            return function () {
                console.log(e);
            }
        })(i), 1000)
    }
    // 使用let
    for (let i = 0; i < 10; i++) {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    }

 

2.JS继承的几种方式

<script>
    var main = document.getElementById('main');

    function log(s, color) {
        this.color = color || '';
        var p = document.createElement('p');
        p.setAttribute('class', this.color);
        p.innerHTML = s;
        main.appendChild(p);
    }


    // JS继承实现方式

    //定义一个动物类
    function Animal(name) {
        //属性
        this.name = name || 'Animal';
        //实例方法
        this.sleep = function () {
            return this.name + '正在睡觉!';
        }
    }

    //原型方法
    Animal.prototype.eat = function (food) {
        return this.name + '正在吃' + food;
    }
    var ani = new Animal('cat');
    //Test Code
    log(ani.eat('fish'));

    // 1.原型链继承
    // 核心:将父类的实例作为子类的原型
    // 特点
    //     父类新增原型方法/原型属性,子类都能访问到
    //     简单、易于实现
    //
    //     无法实现多继承
    //     无法向父类构造函数传参
    log('原型链继承', 'red')

    function Cat() {
    }

    Cat.prototype = new Animal();
    Cat.prototype.name = 'cat';
    Cat.prototype.eateat = function (food) {
        return 'cat又吃' + food;
    }
    //
    //Test Code
    var cat = new Cat();
    log(cat.name);
    log(cat.eat('fish'));
    log(cat.sleep());
    log(cat.eateat('fish'));
    log(cat instanceof Animal); //true
    log(cat instanceof Cat); //true

    // 2.构造继承
    // 核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类
    // 特点:
    //     可以实现多继承(call多个父类对象)
    //     创建子类实例时,可以向父类传递参数
    //
    //     缺点只能继承父类的实例属性和方法,不能继承原型属性/方法
    //     无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
    log('构造继承', 'red')

    function Cat1(name) {
        Animal.call(this);
        this.name = name || 'Tom';
    }

    //Test Code
    var cat1 = new Cat1();
    log(cat1.name);
    log(cat1.sleep());
    log(cat1 instanceof Animal); //false
    log(cat1 instanceof Cat1); //true

    // 3.实例继承
    // 核心:为父类实例添加新特性,作为子类实例返回
    // 不支持多继承
    log('实例继承', 'red');

    function Cat2(name) {
        var instance = new Animal();
        instance.name = name || 'Tom';
        return instance;
    }

    //Test Code
    var cat2 = new Cat2();
    log(cat2.name);
    log(cat2.sleep());
    log(cat2 instanceof Animal); // true
    log(cat2 instanceof Cat2); // false

    // 4.拷贝继承
    // 支持多继承
    // 效率低,内存占用高(因为要拷贝父类属性)
    log('拷贝继承', 'red');
    function Cat3(name) {
        var animal = new Animal();
        for (var p in animal) {
            Cat3.prototype[p] = animal[p];
        }
        Cat3.prototype.name = name || 'Tom';
    }

    // Test Code
    var cat3 = new Cat3();
    log(cat3.name);
    log(cat3.sleep());
    log(cat3 instanceof Animal); // false
    log(cat3 instanceof Cat3); // true

    // 5.组合继承
    // 核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
    // 弥补了方式2的缺陷,可以继承实例属性或方法,也可以继承原型属性和方法
    // 不存在引用属性共享问题
    // 可传参
    // 函数可复用
    // 缺点 调用了两次父类构造函数,生成了两份实例

    log('组合继承', 'red');
    function Cat4(name) {
        Animal.call(this);
        this.name =name || 'Tom';
    }
    Cat4.prototype = new Animal();
    //组合继承也需要修复构造函数指向
    Cat4.prototype.constructor = Cat4;
    var cat4 = new Cat4();
    log(cat4.name);
    log(cat4.sleep());
    log(cat4 instanceof Animal); // true
    log(cat4 instanceof Cat4); // true

    // 6.寄生组合继承
    // 核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法或属性,避免组合继承的缺点
    // 堪称完美
    // 实现较为复杂

    log('寄生组合继承','red');
    function Cat5(name) {
        Animal.call(this);
        this.name = name || 'Tom';
    }
    Cat5.prototype = Object.create(Animal.prototype);
    // Object.create()的polyfill
    /*
    function pureObject(o){
        //定义了一个临时构造函数
         function F() {}
         //将这个临时构造函数的原型指向了传入进来的对象。
         F.prototype = obj;
         //返回这个构造函数的一个实例。该实例拥有obj的所有属性和方法。
         //因为该实例的原型是obj对象。
         return new F();
    }
    */

    Cat5.prototype.constructor =Cat5;

    // Test Code
    var cat5 = new Cat5();
    log(cat5.name);
    log(cat5.sleep());
    log(cat5 instanceof Animal); // true
    log(cat5 instanceof Cat5); //true



    //ES6 继承
    class Animal {
        constructor(name) {
            this.name = name || 'Animal';
            this.sleep = function () {
                console.log('(Animal sleep) -> ' + this.name);
            }
        }

        eat(food) {
            console.log('(Animal eat) -> ' + food + ' -> ' + this.name);
        }
    }

    class Cat extends Animal {
        constructor(name) {
            super(name || 'Cat');
        }

        eatFish(num) {
            num = num || 'a little';
            console.log('(Cat eatFish) -> ' + this.name + ' -> ' + num);
        }

        //重新声明父类同名方法会覆盖,ES5直接在原型链上操作
        eat(food) {
            console.log('(重写的)(Animal eat) -> ' + food + ' -> ' + this.name);
        }
    }

    let animal = new Animal('Dog');
    console.log(animal.name);
    animal.sleep();
    animal.eat('Meat');

    console.log('-----------');

    let cat = new Cat('Big Cat');
    console.log(cat.name);
    cat.sleep();
    cat.eat('Small Fish');
    cat.eatFish('more');

</script>

 

3.JS深浅拷贝

   
    // 引入:

    let a = {
        age: 1
    };
    let b = a;
    a.age = 2;
    console.log(b.age); // 2

    // 上述例子如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。

    // 浅拷贝
    // 1.使用 Object.assign
    // Object.assign是ES6新添加的接口,主要用来合并多个JavaScript对象。如果拷贝过来的属性的值是对象等复合属性
    // 如果拷贝过来的属性的值是对象等复合属性,那么只能拷贝过来一个引用。
    let aa = {
        age: 1
    };
    let bb = Object.assign({}, aa);
    aa.age = 2;
    console.log(aa.age); // 2
    console.log(bb.age); // 1

    // 2.通过展开运算符(...)解决
    let aaa = {
        age: 1
    };
    let bbb = {...aaa};
    aaa.age = 2;
    console.log(aaa.age); // 2
    console.log(bbb.age); // 1

    // 深拷贝  // 所需拷贝的属性的值为对象等复合属性是使用
    // 1.使用 JSON.parse(JSON.stringify(object)) (性能最优)
    let x = {
        age: 1,
        jobs: {
            first: 'first'
        }
    };
    let y = JSON.parse(JSON.stringify(x));
    x.jobs.first = 'second';
    console.log(x.jobs.first); // second
    console.log(y.jobs.first); // first

    // 此方法局限性:

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

 

4.JS拖拽

<script>
    window.onload = function () {
        var oDiv = document.getElementsByTagName("div")[0];

        /*鼠标点击的位置距离DIV左边的距离 */
        var disX = 0;
        /*鼠标点击的位置距离DIV顶部的距离*/
        var disY = 0;
        oDiv.onmousedown = function () {
            var e = e || window.event;
            disX = e.clientX - oDiv.offsetLeft;
            disY = e.clientY - oDiv.offsetTop;

            document.onmousemove = function (e) {
                var e = e || window.event;
                // 横轴坐标
                var leftX = e.clientX - disX;
                // 纵轴坐标
                var topY = e.clientY - disY;

                if (leftX < 0) {
                    leftX = 0;
                }
                /* 获取浏览器视口大小 document.document.documentElement.clientWidth*/
                else if (leftX > document.documentElement.clientWidth - oDiv.offsetWidth) {
                    leftX = document.document.documentElement.clientWidth - oDiv.offsetWidth;
                }

                if (topY < 0) {
                    topY = 0;
                }
                else if (topY > document.documentElement.clientHeight - oDiv.offsetHeight) {
                    topY = document.documentElement.clientHeight - oDiv.offsetHeight;
                }
                oDiv.style.left = leftX + "px";
                oDiv.style.top = topY + "px";
            }
            document.onmouseup = function () {
                document.onmousemove = null;
                document.onmouseup = null;
            }
        }
    }
</script>

 

5.Cookie使用

window.onload = function () {
        document.cookie = "userID=10000";
        document.cookie = "name1=10000";
        //document.cookie = "name2=10000";
        document.cookie = "username=John Smith; expires=Thu, 18 Dec 2019 12:00:00 GMT; path=/";
        // expires:过期时间
        //移除Cookie name2
        //document.cookie = "name2=1;expires=-1";
        alert(document.cookie);

        //exdays: 过期天数
        function setCookie(cname, cvalue, exdays) {
            var d = new Date();
            d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
            var expires = "expires=" + d.toGMTString();
            document.cookie = cname + "=" + cvalue + "; " + expires;
        }

        function getCookie(cname) {
            var name = cname + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i].trim();
                if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
            return "";
        }

        function removeCookie(key) {
            setCookie(key, "", -1);
        }

        removeCookie("name2");
        setCookie('userID', '123', 600);

        alert(getCookie("userID"));
    }

 

6.Promise简化ajax异步处理

function myXHR(method, url, data) {
    var requset = new XMLHttpRequest();
    return new Promise((resolve, reject) => {
        requset.onreadystatechange = function () {
            if (requset.readyState === 4) {
                if (requset.status === 200) {
                    resolve(requset.responseText);
                }
                else {
                    reject(requset.status);
                }
            }
        }
        requset.open(method, url);
        requset.send(data);
    });
}

var p = myXHR('GET', 'url:');
p.then(responseText => {
    console.log(responseText);
}).catch(status => {
    console.log(new Error(status));
})

 

7.RAF-requestAnimationFrame-的使用

<div id="myDiv" style="background-color: lightblue;width: 0;height: 50px;line-height: 50px;">0%</div>
<button id="btn">run</button>

    //显示器刷新率60Hz,so最平滑动画的最佳循环间隔为1000ms/60=16ms
    //requestID = requestAnimationFrame(callback);
    //requestAnimationFrame不需要设置时间间隔
    //回调函数作为参数传入,会返回一个整数(定时器的编号),可以传递给cancelAnimationFrame用于取消这个函数的执行
    //IE 9- 不支持该方法
    var timer1 = requestAnimationFrame(function () {
    });
    var timer2 = requestAnimationFrame(function () {
    });
    var timer3 = requestAnimationFrame(function () {
    });
    console.log(timer1);//1
    console.log(timer2);//2
    console.log(timer3);//3
    //cancelAnimationFrame
    cancelAnimationFrame(timer1);
    cancelAnimationFrame(2);
    cancelAnimationFrame(3);

    //兼容IE
    if (!window.requestAnimationFrame) {
        var lastTime = 0;
        window.requestAnimationFrame = function (callback) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
            var id = window.setTimeout(function () {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        }
    }

    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }

    //示例
    var myDiv = document.getElementById('myDiv');
    var btn = document.getElementById('btn');
    var timer;
    btn.onclick = function () {
        myDiv.style.width = '0';
        cancelAnimationFrame(timer);
        timer = requestAnimationFrame(function fn() {
            if (parseInt(myDiv.style.width) < 500) {
                myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
                myDiv.innerHTML = parseInt(myDiv.style.width) / 5 + ' %';
                timer = requestAnimationFrame(fn);
            } else {
                cancelAnimationFrame(timer);
            }
        });
    }

 

8.js怎么控制一次加载一张图片,加载完后再加载下一张(监控图片是否加载完成)

// 方法一
var obj=new Image();
obj.src="URL";
obj.onload=function(){
    alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
    document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}

// 方法二
var obj=new Image();
obj.src="URL";
obj.onreadystatechange=function(){
    if(this.readyState=="complete"){
        alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height);
        document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
    }
}

 

9.Job queue 的执行顺序

//在Job queue中的队列分为两种类型:macro-task和microTask。

    // macro-task队列包含任务: a1, a2 , a3
    // micro-task队列包含任务: b1, b2 , b3
    //
    // 执行顺序为,首先执行marco-task队列开头的任务,也就是 a1 任务,执行完毕后,
    // 在执行micro-task队列里的所有任务,也就是依次执行***b1, b2 , b3***,执行完后清空micro-task中的任务,
    // 接着执行marco-task中的第二个任务,依次循环。

    //macro-task队列真实包含任务:
    //script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
    //micro-task队列真实包含任务:
    //process.nextTick, Promises, Object.observe, MutationObserver

    //举例
    setTimeout(function () {
        console.log(1)
    }, 0);

    new Promise(function (resolve, reject) {
        console.log(2);
        resolve();
    }).then(function () {
        console.log(3)
    }).then(function () {
        console.log(4)
    });

    process.nextTick(function () {
        console.log(5)
    });

    console.log(6);
    //输出2,6,5,3,4,1
    //这里要注意的一点在定义promise的时候,promise构造部分是同步执行的,这样问题就迎刃而解了。
    // 首先分析Job queue的执行顺序:
    //
    // script(主程序代码)——>process.nextTick——>promise——>setTimeout
    //
    //     I) 主体部分: 定义promise的构造部分是同步的,
    // 因此先输出2 ,主体部分再输出6(同步情况下,就是严格按照定义的先后顺序)
    //
    // II)process.nextTick: 输出5
    //
    //     III)promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是3,4
    //
    //     IV) setTimeout : 最后输出1
    //
    //     综合的执行顺序就是: 2——>6——>5——>3——>4——>1

 

10.原生ajax的请求过程

function getXHR() {
        var xhr = null;
        if (window.XMLHttpRequest) {
            xhr = new XMLHttpRequest();
        } else if (window.ActiveXObject) {
            try {
                xhr = new ActiveXObject('Msxml2.XMLHTTP');//MSXML3
            } catch (e) {
                try {
                    xhr = new ActiveXObject('Microsoft.XMLHTTP')
                }
                catch (e) {
                    alert('不支持');
                }
            }
        }
        return xhr;
    }

    var xhr = getXHR();
    xhr.open('GET', url, true);//true 是否异步
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                var data = xhr.responseText;
                console.log(data);
            }
        }
    }
    xhr.onerror = function () {
        console.log('error');
    }
    xhr.send();//发送请求

 

11.JS检测变量类型

    var str = '123';
    var num = 3;
    console.log(typeof str); //string
    console.log(typeof num); //number

    console.log(typeof str === 'string'); //true
    console.log(typeof(num) === 'number'); //true
    console.log(str.constructor === String); //true

 

12.JS去除字符串空格

    var str = '  asd dasd s d sad asd asc   ';

    //方法一:使用replace正则匹配
    //去除所有空格
    console.log(str.replace(/\s*/g, '')); //asddasdsdsadasdasc

    //去除两头空格
    console.log(str.replace(/^\s*|\s*$/g, '')); //asd dasd s d sad asd asc

    //去除左空格
    console.log(str.replace(/^\s*/, '')); //asd dasd s d sad asd asc

    //去除右空格
    console.log(str.replace(/(\s*$)/g, '')); //  asd dasd s d sad asd asc

    //方法二:使用str.trim();(只能去除左右空格)
    console.log(str.trim()); //asd dasd s d sad asd asc

 

13.获取浏览器URL中查询字符串中的参数

    function showWindowHref(){
        // var sHref = window.location.href;
        var sHref = 'https://www.baidu.com/s?ie=UTF-8&wd=baidu';
        var args = sHref.split('?');
        if(args[0] == sHref){
            return "";
        }
        var arr = args[1].split('&');
        var obj = {};
        for(var i = 0;i< arr.length;i++){
            var arg = arr[i].split('=');
            obj[arg[0]] = arg[1];
        }
        return obj;
    }
    var href = showWindowHref(); // obj
    console.log(href); Object ie: "UTF-8"wd: "baidu"__proto__: Object

 

14.JS字符串操作函数

    var str = 'das12n12n 12 21n';
    var str1 = 'Hello World!', str2 = 'My name is 张三.', str3 = 'I am very Happy.';

    //stringObject.concat(str,...) 连接字符串
    console.log(str1.concat(str2, str3)); //Hello World!My name is 张三.I am very Happy.

    //stringObject.indexOf(searchvalue,fromindex) 方法可返回某个指定的字符串值在字符串中首次出现的位置。规定在字符串中开始检索的位置
    //不存在该字符串返回 -1;
    console.log(str1.indexOf('Hello')); //0
    console.log(str1.indexOf('HELLO')); //-1

    //stringObject.lastIndexOf(searchvalue,fromindex) 方法可返回某个指定的字符串值在字符串中最后出现的位置。规定在字符串中开始检索的位置
    //不存在该字符串返回 -1;
    console.log(str1.lastIndexOf('l')); //9
    console.log(str1.lastIndexOf('0')); //-1

    //stringObject.charAt() 返回指定位置的字符串
    console.log(str1.charAt(1)); //e

    //stringObject.match() 检查一个字符串是否匹配一个正则表达式
    console.log(str.match(/\d+/g));

    //stringObject.substr(start,length) 在字符串中抽取从 start: 下标开始的指定数目的字符 start-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推
    console.log(str1.substr(0, 5)); //Hello
    console.log(str1.substr(-6, 5)); //World

    // stringObject.substring(start,stop) 用于提取字符串中介于两个指定下标之间的字符
    console.log(str1.substring(0, 11)); //Hello World
    console.log(str1.substring(0)); //Hello World!

    //stringObject.slice 提取字符串的一部分,并返回一个新字符串。
    console.log(str1.slice(0, 12)); //Hello World!
    console.log(str1.slice(0)); //Hello World!

    //replace() – 用来查找匹配一个正则表达式的字符串,然后使用新字符串代替匹配的字符串。
    console.log(str1.replace(/\s*/g, '_'));

    //search() – 执行一个正则表达式匹配查找。如果查找成功,返回字符串中匹配的索引值。否则返回 -1 。
    console.log(str1.search('H')); //0
    console.log(str1.search('ooo')); //-1

    //split() – 通过将字符串划分成子串,将一个字符串做成一个字符串数组。
    console.log(str1.split());

    var arr1 = str1.split();
    var arr2 = str2.split();
    var arr = arr1.concat(arr2);
    console.log(arr); //["Hello World!", "My name is 张三."]
    console.log(arr[1]); //My name is 张三.

    //toLowerCase() – 将整个字符串转成小写字母

    //toUpperCase() – 将整个字符串转成大写字母

 

15.JS创建、添加、移除、移动、复制、创建和查找节点

<div id="div1"></div>
<button id="btn1">add</button>
<button id="btn2">remove</button>
<button id="btn3">replace</button>
    var div1 = document.querySelector('#div1');
    var btn1 = document.querySelector('#btn1');
    var btn2 = document.querySelector('#btn2');
    var btn3 = document.querySelector('#btn3');

    //创建新节点
    //创建一个DOM片段
    var node1 = document.createDocumentFragment('<h1>标题1</h1>');

    var browsers = ['Firefox', 'Chrome', 'Opera',
        'Safari', 'Internet Explorer'];

    browsers.forEach(function (browser) {
        var li = document.createElement('li');
        li.textContent = browser;
        node1.appendChild(li);
    });

    //创建一个具体元素
    var node2 = document.createElement('p');

    //创建一个文本节点
    var node3 = document.createTextNode('这是一个文本节点;HTML由元素节点和文本节点构成');

    node2.appendChild(node3);

    // 添加、移除、替换、插入
    //添加
    btn1.onclick = function () {
        div1.appendChild(node1);
        div1.appendChild(node2);
    };

    //移除
    btn2.onclick = function () {
        div1.removeChild(node2);
    }

    //替换 replaceChild(new, old);
    btn3.onclick = function () {
        var node3 = document.createElement('h1');
        node3.innerText = '标题';
        div1.replaceChild(node3, node2);
    }

    //插入 inertBefore();inertAfter();

    //查找
    document.getElementById();
    document.getElementsByName();
    document.getElementsByTagName();

 

16.JS的各种位置

    //clientHeight表示的是可视区域的高度,不包含border和滚动条(css height + css padding)
    console.log('clientHeight:'+document.getElementById('div').clientHeight);
    //Element.scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容
    console.log('scrollHeight:'+document.getElementById('div').scrollHeight);
    //HTMLElement.offsetHeight 是一个只读属性,它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数
    console.log('offsetHeight:'+document.getElementById('div').offsetHeight);
    //clientTop一个元素顶部边框的宽度(以像素表示)。不包括顶部外边距或内边距。clientTop 是只读的
    console.log('clientTop:'+document.getElementById('div').clientTop);
    //Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。
    console.log('scrollTop:'+document.documentElement.scrollTop);
    //HTMLElement.offsetTop 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部的距离
    console.log('offsetTop:'+document.getElementById('div').offsetTop);
    

 

17.内存泄漏问题

// 定义和用法

    // 内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。
    // 浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

    // 内存泄漏的几种情况
    // 1.当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。

    //实例如下
    var btn = document.getElementById("myBtn");
    // btn.onclick = function () {
    //     document.getElementById("myDiv").innerHTML = "Processing...";
    // }

    // 解决方法

    btn.onclick = function () {
        /******************
         * btn.onclick = null;
         *
         */
        btn.onclick = null;

        document.getElementById("myDiv").innerHTML = "Processing...";
    }

    //2.由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包。闭包可以维持函数内局部变量,使其得不到释放。

    //实例如下
    function bindEvent() {
        var obj = document.createElement('XXX');
        obj.onclick = function () {
            // Even if it is a empty function
        };
    }

    //解决方法
    function bindEvent() {
        var obj = document.createElement('XXX');
        obj.onclick = function () {
            // Even if it is a empty function
        };
        obj = null;
    }

 

18.JS面向对象中继承的实现(ES5和ES6)

    //ES5:寄生组合式继承:通过借用构造函数来继承属性和原型链来实现子继承父。
    function Animal(name) {
        this.name = name || 'Animal';
        this.sleep = function () {
            console.log('(Animal sleep) -> ' + this.name);
        }
    }

    Animal.prototype.eat = function (food) {
        console.log('(Animal eat) -> ' + food + ' -> ' + this.name);
    }

    function Cat(name) {
        Animal.call(this);
        this.name = name || 'Cat';
    }

    Cat.prototype = Object.create(Animal.prototype);
    Cat.prototype.constructor = Cat;
    Cat.prototype.eatFish = function (num) {
        num = num || 'a little';
        console.log('(Cat eatFish) -> ' + this.name + ' -> ' + num);
    };

    // Object.create()的polyfill
    /*
    function pureObject(o){
        //定义了一个临时构造函数
         function F() {}
         //将这个临时构造函数的原型指向了传入进来的对象。
         F.prototype = obj;
         //返回这个构造函数的一个实例。该实例拥有obj的所有属性和方法。
         //因为该实例的原型是obj对象。
         return new F();
    }
    */

    let animal = new Animal('Dog');
    console.log(animal.name); //Dog
    animal.sleep(); //(Animal sleep) -> Dog
    animal.eat('Meat'); //(Animal eat) -> Meat -> Dog

    console.log('-----------'); //-----------

    let cat = new Cat('Big Cat');
    console.log(cat.name); //Big Cat
    cat.sleep(); //(Animal sleep) -> Big Cat
    cat.eat('Small Fish'); //(Animal eat) -> Small Fish -> Big Cat
    cat.eatFish('more'); //(Cat eatFish) -> Big Cat -> more

    // ES6
    class Animal {
        constructor(name) {
            this.name = name || 'Animal';
            this.sleep = function () {
                console.log('(Animal sleep) -> ' + this.name);
            }
        }

        eat(food) {
            console.log('(Animal eat) -> ' + food + ' -> ' + this.name);
        }
    }

    class Cat extends Animal {
        constructor(name) {
            super(name || 'Cat');
        }

        eatFish(num) {
            num = num || 'a little';
            console.log('(Cat eatFish) -> ' + this.name + ' -> ' + num);
        }

        //重新声明父类同名方法会覆盖,ES5直接在原型链上操作
        eat(food) {
            console.log('(重写的)(Animal eat) -> ' + food + ' -> ' + this.name);
        }
    }

    let animal = new Animal('Dog');
    console.log(animal.name); //Dog
    animal.sleep(); //(Animal sleep) -> Dog
    animal.eat('Meat'); //(Animal eat) -> Meat -> Dog

    console.log('-----------'); //-----------

    let cat = new Cat('Big Cat');
    console.log(cat.name); //Big Cat
    cat.sleep(); //(Animal sleep) -> Big Cat
    cat.eat('Small Fish'); //(重写的)(Animal eat) -> Small Fish -> Big Cat
    cat.eatFish('more'); //(Cat eatFish) -> Big Cat -> more

 

19.JS Array对象

    var arr1 = [1, 2, 3, 41, 4, 3, 312, 3, 123];

    console.log(arr1.constructor);
    console.dir(arr1.constructor);

    // Array对象方法
    // join() 用于把数组中的所有元素放入一个字符串。
    var arr = new Array(3)
    arr[0] = "George"
    arr[1] = "John"
    arr[2] = "Thomas"

    console.log(arr.join(".")); //George.John.Thomas

    //pop() 删除并返回数组的最后一个元素
    console.log(arr1.pop()); //123

    //shift() 删除并返回数组的第一个元素

    // push() 向数组的末尾添加一个或更多元素,并返回新的长度。
    var arrPush = ['first', 'second'];
    console.log(arrPush.push('third', 'fourth'));
    console.log(arrPush); //(4) ["first", "second", "third", "fourth"]

    // unshift() 向数组的开头添加一个或更多元素,并返回新的长度。

    // reverse() 颠倒数组中元素的顺序
    console.log(arrPush.reverse()); //(4) ["fourth", "third", "second", "first"]

    // splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目
    // arrayObject.splice(index,howmany,item1,.....,itemX)
    // index	必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
    // howmany	必需。要删除的项目数量。如果设置为 0,则不会删除项目。
    // item1, ..., itemX	可选。向数组添加的新项目。

    // toSource() 返回该对象的源代码。
    // 只有 Gecko 核心的浏览器(比如 Firefox)支持该方法,也就是说 IE、Safari、Chrome、Opera 等浏览器均不支持该方法。
    function employee(name, job, born) {
        this.name = name;
        this.job = job;
        this.born = born;
    }

    var bill = new employee("Bill Gates", "Engineer", 1985);
    console.log(bill.toSource());

    // toString() 把数组转换为字符串,并返回结果。

    // toLocaleString() 把数组转换为本地数组,并返回结果。

    // valueOf() 返回数组对象的原始值

 

20.数组去重的几种方法

    var arr = [12, 1, 1, 1, 3, 1, 3, 12, 3, 14, 2, 45, 2, 34, 21, 3, 12, 3, 21312, 3];
    var obj = {};
    var tmp = [];//去重后
    var rec = [];//重复的

    //方法一
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            obj[arr[i]] = 1;
            tmp.push(arr[i]);
        } else {
            rec.push(arr[i]);
        }
    }
    console.log(tmp);
    // 0: 12
    // 1: 1
    // 2: 3
    // 3: 14
    // 4: 2
    // 5: 45
    // 6: 34
    // 7: 21
    // 8: 21312
    console.log(rec);
    // 0: 1
    // 1: 1
    // 2: 1
    // 3: 3
    // 4: 12
    // 5: 3
    // 6: 2
    // 7: 3
    // 8: 12
    // 9: 3
    // 10: 3

    // 方法二
    var tmp2 = [];
    var rec2 = [];
    for (let i = 0; i < arr.length; i++) {
        if (tmp2.indexOf(arr[i]) < 0) {
            tmp2.push(arr[i]);
        }
        else {
            rec2.push(arr[i]);
        }

    }
    console.log(tmp2);
    // 0: 12
    // 1: 1
    // 2: 3
    // 3: 14
    // 4: 2
    // 5: 45
    // 6: 34
    // 7: 21
    // 8: 21312
    console.log(rec2);
    // 0: 1
    // 1: 1
    // 2: 1
    // 3: 3
    // 4: 12
    // 5: 3
    // 6: 2
    // 7: 3
    // 8: 12
    // 9: 3
    // 10: 3

    //方法3
    var tmp3 = arr.filter(function (element, index, self) {
        return self.indexOf(element) === index;
    });
    console.log(tmp3);
    // 0: 12
    // 1: 1
    // 2: 3
    // 3: 14
    // 4: 2
    // 5: 45
    // 6: 34
    // 7: 21
    // 8: 21312

 

21.防抖

 <input type="text" id="text">
    const text = document.getElementById('text');
    text.oninput = debounce(search, 500);

    let flag = 0;

    function search() {
        flag++;
        console.log(`发起请求${flag}次`);
    }

    function debounce(fn, delay) {
        var timer = null;
        return function () {
            // 通过 ‘this’ 和 ‘arguments’ 获取函数的作用域和变量
            var context = this;
            var args = arguments;
            // 清理掉正在执行的函数,并重新执行
            clearTimeout(timer);
            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        }
    }

    // function debounce(fn, delay) {
    //     let timer
    //     return function (...args) {
    //         if (timer) clearTimeout(timer)
    //         timer = setTimeout(() => {
    //             fn.apply(this, args)
    //         }, delay)
    //     }
    // }

 

二、HTML和CSS部分

 

1.清除浮动的几种方式

        /* 最好方法使用:after */

        div:after{
            content: '';
            clear: both;
            display: block;
            width: 0;
            height: 0;
        }

        /* 或者新建一个空元素来清除浮动 */

        .clear{
            clear: both;
            height: 0;
            line-height: 0;
            font-size: 0;
        }

        /* 给父元素增加overflow属性 */

        .over-flow{
            over-flow: auto;
            zoom: 1; /* 触发IE hasLayout, 处理兼容性问题*/
        }

2.引入样式link与import区别

    <!--link方式-->
    <link rel="stylesheet" type="text/css" href="style.css">

    <!--import方式-->
    <style type="text/css">
        @import url(style01.css);
    </style>
  • 区别1:link是XHTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS。
  • 区别2:link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载。
  • 区别3:link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
  • 区别4:ink支持使用Javascript控制DOM去改变样式;而@import不支持。

3.CSS画三角形

div{
            width: 0;
            height: 0;
            border: 100px solid transparent;
            border-bottom-color: red;/*下边框,向上的三角形*/
        }

4.不使用border新建一个一像素的直线

<div style="height:1px; background-color: red; overflow: hidden; width: 100%"></div>

 

5.HTML5新特性

<li>语义化标签 nav header footer section aside</li>
<li>绘图的canvas</li>
<li>媒体的video和audio</li>
<li>localStorage与sessionStorage</li>
<li>表单控件:calendar、date、time、email、url、search</li>
<br/>
<h3>html5 与 html可以使用标签或者doctype区分</h3>
<br>
<h3>语义化使HTML结构更清晰,便于浏览器解析,利于SEO搜索,使代码更好理解,便于维护</h3>

 

6.CSS3动画

            /*2D转换 transform*/
            transform: translate(-20px, -20px); /*坐标内移动*/
            transform: rotate(45deg); /*旋转*/
            transform: scale(0.8); /*缩放*//*给定的宽度(X 轴)和高度(Y 轴)参数。transform: scale(2,4);*/
            transform: skew(0, 20deg); /*倾斜*/
            transform: matrix(0, 0, 0, 0, 0, 0); /*把所有 2D 转换方法组合在一起,需要六个参数,包含数学函数,允许您:旋转、缩放、移动以及倾斜元素*/

            /*3D转换*/
            transform: rotateX(120deg); /*元素围绕其 X 轴以给定的度数进行旋转*/
            transform: rotateY(120deg); /*元素围绕其 Y 轴以给定的度数进行旋转*/

            /*transition 过渡效果*/
            transition-property: all; /*执行动画对应属性 color background*/
            transition-duration: 5s; /*动画持续时间*/
            transition-timing-function: linear; /*动画变化的速率 ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier */
            transition-delay: 5s; /*延迟多久开始动画*/
            transition: all 5s linear 5s;

            /*animation 动画*/
            animation-name: name;
            animation-duration: 5s;
            animation-timing-function: linear;
            animation-delay: 5s;
            animation-iteration-count: infinite; /*指定元素播放动画的循环次数 infinite | <number>*/
            animation-direction: normal; /*指定元素动画播放的方向,其只有两个值,默认值为normal,如果设置为normal时,动画的每次循环都是向前播放;另一个值是alternate,他的作用是,动画播放在第偶数次向前播放,第奇数次向反方向播放。*/
            animation-play-state: paused; /*控制元素动画的播放状态*/
            animation: name 5s linear 5s infinite normal paused;

 

7.CSS垂直和水平居中的几种方式

<section style="position: relative">
    不固定宽高<br/>
    position: absolute;<br/>
    top: 50%;<br/>
    left: 50%;<br/>
    margin-left: -25%;<br/>
    margin-top: -25%;<br/>
    <style>
        #div1{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            position: absolute;
            top: 50%;
            left: 50%;
            margin-left: -25%;
            margin-top: -25%;
            background-color: #666666;
        }
    </style>
    <div id="div1">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>
<section>
    不固定宽高<br/>
    transform:translate(50%,50%)
    <style>
        #div2{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            transform:translate(50%,50%);
            background-color: #666666;

        }
    </style>
    <div id="div2">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>
<section style="position: relative">
    固定宽高<br/>
    position: absolute;<br/>
    left: 0;<br/>
    top: 0;<br/>
    bottom: 0;<br/>
    right: 0;<br/>
    margin: auto;<br/>
    <style>
        #div3{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            background-color: #666666;
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            right: 0;
            margin: auto;

        }
    </style>
    <div id="div3">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>
<section style="display: flex;
            justify-content: center;
            align-items: center;">

    父元素使用:<br/>
    display: flex;<br/>
    justify-content: center;<br/>
    align-items: center;<br/>
    <style>
        #div4{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            background-color: #666666;
        }
    </style>
    <div id="div4">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>
<section style="display: grid">
    grid布局<br/>
    父元素:display:grid;<br/>
    子元素:align-self: center;<br/>
    justify-self: center;<br/>
    <style>
        #div5{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            background-color: #666666;
            align-self: center;
            justify-self: center;

        }
    </style>
    <div id="div5">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>
<section style="display: table">
    父盒子宽高为100%<br/>
    table布局<br/>
    父元素:display:table;<br/>
    子元素:display:table-cell
    text-align: center;<br/>
    verical-align: middle;<br/>
    <style>
        #div6{
            width:15vw;
            height: 15vw;
            overflow: hidden;
            background-color: #666666;
            display: table-cell;
            text-align: center;
            vertical-align: middle;

        }
    </style>
    <div id="div6">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores aut deserunt incidunt itaque voluptate. Enim et eum, neque nihil officia perspiciatis suscipit tenetur voluptatem voluptatum! Architecto dolorem doloribus perspiciatis vitae.
    </div>
</section>

 

三、开发性能优化

1.规避JavaScript多人开发函数重名问题

    // 1.命名空间
    // var MYNAMESPACE=MYNAMESPACE||{};
    // 若全局空间中已有同名对象,则不覆盖该对象;否则创建一个新的命名空间。

    // 举例
    var MYNAMESPACE=MYNAMESPACE||{};

    MYNAMESPACE.person=function (name) {
        this.name=name;
    };
    MYNAMESPACE.person.prototype.getName=function () {
        return this.name;
    };

    var p=new MYNAMESPACE.person('NAME');
    console.log(p.getName());


    // 2.封闭空间
    // js中的封闭空间主要是利用了给变量加括号结果不变

    // 书写方式
    ;(function () {
        // code...
    })();
    // 在函数前面加分号是为了避免被别人坑,导致和别的程序猿发生肢体冲突:
    // 如果别人的代码没有写分号,如果代码压缩的时候就会发生问题,
    // 这个时候我们自己的代码就会拯救我们,而两个分号写在一起时是没有问题的。

    // 3.JS模块化MVC(数据层、表现层、控制层)

    // 4.seajs
    // SeaJS是一个遵循CMD规范的JavaScript模块加载框架,可以实现JavaScript的模块化开发及加载机制


    // 5.变量转化成对象的属性
    var WMD={};
    WMD['name']='张三';

    WMD['person']=function (name) {
        this.name=name;
    };

    WMD.person.prototype.getName=function () {
        return this.name;
    };

    var _p=new WMD.person('Tom');
    console.log(_p.getName());
    console.log(WMD);

    // 6.对象化

 

2.降低页面加载时间的方法

(1) 压缩css、js文件

// 在线压缩工具:http://tool.oschina.net/jscompress
// 由后端动态生成或工具直接生成(grunt+requirejs)

(2) 合并js、css文件,减少http请求

// 页面引入的的js,css越多的话,那么对就增加了http请求数

// 以合并JS文件为例,使用bat批处理命令
// 新建.bat批处理文件 内容 /b:固定参数
// 语法 cope 文件1.文件类型+文件2.文件类型 合并后文件名.文件类型 \b
// 例如 copy G.js+T.JS GT_bin.js /b

// 由后端动态生成或工具直接生成(grunt+requirejs)

(3) 外部js、css文件放在最底下

(4) 减少dom操作,尽可能用变量替代不必要的dom操作

 

3.web前端提高页面性能优化

  针对HTML

        1. 避免再HTML中直接写css代码。

        2. 使用Viewport加速页面的渲染。

        3. 使用语义化标签,减少css的代码,增加可读性和SEO。

        4. 减少标签的使用,dom解析是一个大量遍历的过程,减少无必要的标签,能降低遍历的次数。

        5. 避免src、href等的值为空。

        6. 减少dns查询的次数。
        7. 避免再HTML中直接写css代码。

   针对CSS:

        1.优化选择器路径:
            相比于 .a .b .c{} ,更倾向于大家写.c{}

        2.压缩文件

        3.选择器合并
            把有共同的属性内容的一系列选择器组合到一起

        4.精准样式  减少不必要的属性设置
            比如你只要设置{padding-left:10px}的值,那就避免{padding:0 0 0 10px}这样的写法

        5.雪碧图

        6.避免通配符
            .a .b *{} 像这样的选择器,根据从右到左的解析顺序在解析过程中遇到通配符(*)会回去遍历整个dom

        7.少用float
            Float在渲染时计算量比较大,尽量减少使用。

        8. 0 值去单位

        9.把 CSS 放到代码页上端

  针对JavaScript :

        1. 脚本放到 HTML 代码页底部 (Put Scripts at the Bottom)

        2. 尽可能合并script代码

        3. css能干的事情,尽量不要用JavaScript来干。

        4. 尽可能压缩的js文件,减少资源下载的负担

        5. 尽可能避免在js中逐条操作dom样式,尽可能预定义好css样式,然后通过改变样式名来修改dom样式,这样集中式的操作能减少reflow或repaint的次数。

        6. 尽可能少的在js中创建dom,而是预先埋到HTML中用display:none来隐藏,在js中按需调用,减少js对dom的暴力操作。

  面向图片(Image):

        1.优化图片

        2 不要在 HTML 中使用缩放图片

        3 使用恰当的图片格式

<!--作者:顾家进-->
<!--链接:https://juejin.im/post/5bbaa549e51d450e827b6b13-->
<!--来源:掘金-->
 

4.图像优化 图片格式区别

//     优化图像:
//     1、不用图片,尽量用css3代替。 比如说要实现修饰效果,如半透明、边框、圆角、阴影、渐变等,在当前主流浏览器中都可以用CSS达成。
//    
//     2、 使用矢量图SVG替代位图。对于绝大多数图案、图标等,矢量图更小,且可缩放而无需生成多套图。现在主流浏览器都支持SVG了,所以可放心使用!
//    
//     3.、使用恰当的图片格式。
//     我们常见的图片格式有JPEG、GIF、PNG。
//     基本上,内容图片多为照片之类的,适用于JPEG。
//     而修饰图片通常更适合用无损压缩的PNG。
//     GIF基本上除了GIF动画外不要使用。且动画的话,也更建议用video元素和视频格式,或用SVG动画取代。
//    
//     4、按照HTTP协议设置合理的缓存。
//    
//     5、使用字体图标webfont、CSS Sprites等。
//    
//     6、用CSS或JavaScript实现预加载。
//    
//     7、WebP图片格式能给前端带来的优化。WebP支持无损、有损压缩,动态、静态图片,压缩比率优于GIF、JPEG、JPEG2000、PG等格式,非常适合用于网络等图片传输。
//    
//     图像格式的区别:
//     矢量图:图标字体,如 font-awesome;svg
//    
//     位图:gif,jpg(jpeg),png
//    
//     区别:
//
//   1、gif:是是一种无损,8位图片格式。具有支持动画,索引透明,压缩等特性。适用于做色彩简单(色调少)的图片,如logo,各种小图标icons等。
//
//   2、JPEG格式是一种大小与质量相平衡的压缩图片格式。适用于允许轻微失真的色彩丰富的照片,不适合做色彩简单(色调少)的图片,如logo,各种小图标icons等。
//
//   3、png:PNG可以细分为三种格式:PNG8,PNG24,PNG32。后面的数字代表这种PNG格式最多可以索引和存储的颜色值。
//
//     关于透明:PNG8支持索引透明和alpha透明;PNG24不支持透明;而PNG32在24位的PNG基础上增加了8位(256阶)的alpha通道透明;
//
//     优缺点:
//
//   1、能在保证最不失真的情况下尽可能压缩图像文件的大小。
//
//   2、对于需要高保真的较复杂的图像,PNG虽然能无损压缩,但图片文件较大,不适合应用在Web页面上。

5.浏览器渲染页面流程

浏览器渲染过程.png

  1.解析HTML文件,创建DOM树。
     浏览器通过HTMLParser(HTML解析器)根据深度遍历的原则把HTML解析成DOM Tree。
     自上而下,遇到任何样式(link、style)与脚本(script)都会阻塞(外部样式不阻塞后续外部脚本的加载)。

  2.解析CSS。
     将CSS解析成CSS Rule Tree(CSSOM Tree)。
     优先级:浏览器默认设置<用户设置<外部样式<内联样式<HTML中的style样式;

  3.将CSS与DOM合并,构建渲染树(Render Tree)
     根据DOM树和CSSOM树来构造render Tree。

  4.布局和绘制,重绘(repaint)和重排(reflow)
     layout:根据得到的render tree来计算所有节点在屏幕的位置。
     paint:遍历render tree,并调用硬件图形API来绘制每个节点。

(部分未完成,待更新)

  • 76
    点赞
  • 301
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值