1.JS中的变量

(1)变量提升和函数提升

变量提升和函数提升的过程:

  • 创建阶段:JS解释器会找出需要提升的变量和函数,并给他们在内存中开辟好空间,变量的话会声明并且赋值为undefined,而函数会整个存储在内存中
  • 代码执行阶段:就可以使用上面进行提升的变量和函数了

注意:let也会提升,但是let存在暂时性死区,所以在声明之前不可使用

(2)let,var,const区别

参考这篇文章:let与var的区别

let定义的变量不会挂载到window对象上,而是形成一个块作用域,而var定义的变量会挂在到window上

2.apply,call,bind

(1)apply,call,bind作用

  • 改变函数内部的this指向

(2)apply,call,bind区别

  • apply接收的是一个数组类型的参数,call和bind接收的是一个参数列表
  • apply和call返回的是一个值结果,而bind返回的是一个函数,需要再次进行调用
方法 语法 参数 返回值
apply func.apply(thisArg, [argsArray]) this,数组或类数组对象 结果值
call function.call(thisArg, arg1, arg2, …) this,参数列表 结果值
bind function.bind(thisArg, arg1, arg2, …) this,参数列表 函数

bind()方法返回的函数没有prototype属性,为什么没有prototype属性呢,因为如果把这个返回的函数做构造函数的话,那就需要将创建对象的this指向这个函数,但是这个函数在bind调用前就将this指向调用对象了,那么创建对象时又将这个this指向创建对象,不就矛盾了吗,所以bind()返回的函数没有prototype属性。

        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this);
            this.d = 50;
        }
        Function.prototype.myBind = function () {
            if (typeof this !== 'function') {
                throw new TypeError('调用者必须是函数')
            }
            var arg = arguments[0],
                slice = Array.prototype.slice,
                args = slice.call(arguments, 1),
                _that = this;
            return function () {
                return _that.apply(arg, args.concat(slice.call(arguments)));
            }
        }
        var c = new getInfo.myBind(obj, 'Software Engineer'); // Uncaught TypeError: 调用者必须是函数

如果bind方法作为构造函数的话,那么它里面的this就需要指向创建的对象,而bind中的this必须指向函数,所以此时就会报错。

apply,call,bind用法展示:

        let obj = {
            age: 18,
            job: 'student'
        }
        function getInfo(name) {
            console.log(name); // 小花
            console.log(this.age); // 18
            console.log(this.job); // student
        }
        // getInfo.apply(obj, ['小花']);
        // getInfo.call(obj, '小花');
        getInfo.bind(obj, '小花')();
        let obj = {
            age: 18
        }
        function getInfo(job, height) {
            console.log(job);
            console.log(height);
            console.log(this.age);
        }
        getInfo.bind(obj, 'student')(173);

(3)手写apply()

        let obj = {
            name: '小王'
        }
        function getInfo(job) {
            console.log(this.name);
            console.log(job);
        }
        // getInfo.apply();
        Function.prototype.myApply = function (context) {
            context = context || window;
            context.fn = this;
            let result;
            if (arguments[1]) {
                if (Array.isArray(arguments[1])) {
                    result = context.fn(...arguments[1]);
                } else {
                    console.error('Uncaught TypeError: CreateListFromArrayLike called on non-object');
                }

            } else {
                result = context.fn()
            }
            delete context.fn;
            return result;
        }
        getInfo.myApply(obj, ['student']);

(4)手写call()

        let obj = {
            age: 18
        }
        function getInfo(job, name) {
            console.log(job);
            console.log(name);
            console.log(this.age);
        }
        // getInfo.call(obj, 'student');
        Function.prototype.myCall = function (context) {
            context = context || window;
            context.fn = this;
            let result;
            if (arguments[1]) {
                result = context.fn(...[...arguments].slice(1));
            } else {
                result = context.fn();
            }
            delete context.fn;
            return result;
        }
        getInfo.myCall(obj, 'student', '小王');

(5)bind()

  • bind()的使用
        let obj = {
            age: 18
        }
        function getInfo(job, height) {
            console.log(job);
            console.log(height);
            console.log(this.age);
        }
        getInfo.bind(obj, 'student')(173);
  • bind()返回的函数用new操作符调用
        function Point(x, y) {
            this.x = x;
            this.y = y;
        }
        Point.prototype.toString = function () {
            return this.x + ',' + this.y;
        }
        let p = new Point(1, 2);
        p.toString();
        let emptyObj = {};
        let YAxisPoint = Point.bind(emptyObj, 0);
        let axisPoint = new YAxisPoint(5);
        axisPoint.toString();
  • Polyfill中bind()的实现

第一种😎😎:

        if (!Function.prototype.bind) (function () {
            var slice = Array.prototype.slice;
            Function.prototype.bind = function () {
                var thatFunc = this, thatArg = arguments[0];
                var args = slice.call(arguments, 1);
                if (typeof thatFunc !== 'function') {
                    throw new TypeError('Function.prototype.bind-' +
                        'what is trying to be bound is not callable');
                }
                return function () {
                    var funcArgs = args.concat(slice.call(arguments));
                    return thatFunc.apply(thatArg, funcArgs);
                }
            }
        })()	

仿写这种实现bind():

        var obj = {
            age: 25
        }
        function getInfo(name, job) {
            console.log(name);
            console.log(job);
            console.log(this.age);
        }
        // getInfo.bind(obj, '星空海绵')('Software Engineer');
        Function.prototype.myBind = function () {
            if (typeof this !== 'function') {
                throw new TypeError('调用者必须是函数')
            }
            var arg = arguments[0];
            var slice = Array.prototype.slice
            var args = slice.call(arguments, 1);
            var _that = this;
            return function () {
                return _that.apply(arg, args.concat(slice.call(arguments)))
            }
        }
        getInfo.myBind(obj, '星空海绵')('Software Engineer');

上面定义变量为什么要用var呢,因为用的是ES5,如果用let,都有ES6了,那我就直接用bind()了,而不是手写一个了。

我们会发现使用上面这种手写bind()不支持new调用构造函数创建新的对象

        function Point(x, y) {
            this.x = x;
            this.y = y;
        }
        Point.prototype.toString = function () {
            console.log('toString方法');
        }
        Function.prototype.myBind = function () {
            if (typeof this !== 'function') {
                throw new TypeError('调用者必须是函数')
            }
            var arg = arguments[0];
            var slice = Array.prototype.slice
            var args = slice.call(arguments, 1);
            var _that = this;
            return function () {
                return _that.apply(arg, args.concat(slice.call(arguments)))
            }
        }
        let YAxisPoint = Point.myBind({}, 0);
        let axisPoint = new YAxisPoint(5);
        console.dir(axisPoint);
        axisPoint.toString();

看下上面这个代码的axisPoint是啥

我们再看如果把上面代码中的myBind换成bind

也就是说上面这种写法不支持new调用构造函数创建新的对象,所以说有了下面第二种方法。

注意,当使用new操作符绑定构造函数时,会忽略第一个参数

        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this); // {age: 25}
        }
        getInfo.bind(obj, 'Software Engineer')();

以上是没有用new调用的时候,getInfo中的this是指向obj的,没问题

        function A() {
            this.a = 100;
            console.log(this); // {a: 100}
        }
        let b = new A();
        console.log(b); // {a: 100}

        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this); // getInfo {}
        }
        var a = getInfo.bind(obj, 'Software Engineer');
        var b = new a();

以上的代码你会发现getInfo中的this并不是obj,而是getInfo对象,上面我们也看到有说,当使用new操作符号时会忽略掉第一个参数,也就是让调用bind的函数中的this指向的第一个参数被忽略掉,然后我们再想想bind的作用,不就是改变函数得this指向吗,现在第一个参数都忽略了,那么bind是不是相当于没用了,仔细想想,确实可以这个理解。

        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this); // getInfo {}
        }
        var a = getInfo.bind(obj, 'Software Engineer');
        var b = new a();
        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this); // getInfo {}
        }
        var b = new getInfo('Software Engineer');
        console.log(b);

相对而言,上面这两段代码其实通过构造函数创建的b对象时一样的,也就是说当使用了new操作符号时,bind就没用,就是单纯的返回一个函数。理解了这个我们就会明白为什么之前的代码会存在不知new调用构造函数创建新对象了,那么就有了第二种方法。

当使用new调用构造函数时,构造函数中的this是指向新创建的对象的

第二种😯😯:


使用ES6的new.target加以理解

        var obj = {
            age: 25
        }
        function getInfo(job, height) {
            console.log(this);
            this.d = 50;
        }
        Function.prototype.myBind = function () {
            if (typeof this !== 'function') {
                throw new TypeError('调用者必须是函数')
            }
            var arg = arguments[0],
                slice = Array.prototype.slice,
                args = slice.call(arguments, 1),
                _that = this;
            var fn1 = {};
            fn1 = new getInfo();
            return function () {
                if (new.target) {
                    return fn1
                } else {
                    return _that.apply(arg, args.concat(slice.call(arguments)));
                }
            }
        }
        var c = getInfo.myBind(obj, 'Software Engineer');
        var d = new c();
        console.log(d);

3.原型链

(1)对象和函数的原型

每个对象都有一个原型,但是原型没有提供可以直接访问的属性,所以浏览器实现了一个__proto__属性,可以直接访问到对象的原型,每个函数都有一个__proto__和prototype属性,当函数当对象使用时,原型是__proto__,当函数作为构造函数使用时,原型是prototype。

  • 当函数作为对象使用时
        function Demo() { }
        Demo.__proto__.show = function () {
            console.log("__proto__show");
        }
        Demo.prototype.read = function () {
            console.log("prototype-read");
        }
        Demo.show();  // __proto__show
        Demo.read(); // Demo.read is not a function
  • 当函数作为构造函数使用时
        function Demo() { }
        let obj = new Demo();
        Demo.prototype.read = function () {
            console.log("read");
        }
        Demo.__proto__.show = function () {
            console.log("show");
        }
        obj.read(); // read
        obj.show(); // obj.show is not a function

(2)原型链

对象可以通过__proto__属性找到不属于该对象的属性,__proto__将对象连接起来组成了原型链。

        function A() { }
        function B() { }
        function C() { }
        C.prototype.show = function () {
            console.log('C构造函数');
        }
        B.prototype = new C();
        A.prototype = new B();
        let a = new A();
        a.show(); // C构造函数

4.如何判断对象类型

(1)通过 Object.prototype.toString.call()

        let obj = {
            age: 20
        }
        let arr = [15, 20, 25];
        console.log(Object.prototype.toString.call(obj)); // [object Object]
        console.log(Object.prototype.toString.call(arr)); // [object Array]

(2)通过instanceof

instanceof检测构造函数的prototype属性是不是出现在某个对象的原型链上

        function A() { };
        function B() { };
        const b = new B();
        A.prototype = b;
        const a = new A();
        console.dir(a instanceof A); // true

5.箭头函数

        var age = 10;
        let obj = {
            age: 20,
            say: () => {
                return this.age;
            }
        }
        console.log(obj.say()); // 10
        let obj1 = {
            age: 30
        }
        obj1 = Object.create(obj);
        obj1.age = 30;
        console.log(obj1.say()); // 10

通过上面的代码我们会发现箭头函数say中的this并不会因为调用对象的改变而改变,而是一直指向全局对象window的。

        function getName() {
            let say = () => {
                console.log(this.age);
            }
            say();
        }
        let obj = {
            age: 20,
            getName: getName
        }
        obj.getName(); // 20

箭头函数没有自己的this,它只会从自己作用域链的上层继承this。

箭头函数中不能使用arguments和super以及new.target,同时箭头函数也不能作为构造函数。

6.this

(1)谁调用函数,函数里得this就指向谁

        function getAge() {
            console.log(this.age)
        }
        var age = 1
        getAge()

        var obj = {
            age: 2,
            getAge: getAge
        }
        obj.getAge()

(2)new构造函数创建对象时,构造函数内部的this指向创建的对象

(3)可以通过apply,call,bind来改变函数内部的this指向

7.async和await

(1)async和await对比Promise

async和await相比Promise,优势在于处理then调用链,代码更清晰,缺点是乱用await会导致性能问题,因为await会阻塞代码。

        var a = 0;
        var b = async () => {
            a = a + await 10;
            console.log('2', a);
            a = (await 10) + a;
            console.log('3', a);
        }
        b();
        a++;
        console.log('1', a);

待深入

8.generator

(1)generator是ES6中新增的语法,和Promise一样,都可以用来进行异步编程

        function* test() {
            let a = 1 + 2;
            yield 2;
            yield 3;
        }
        let b = test();
        console.log(b.next());
        console.log(b.next());
        console.log(b.next());

(2)generator原理,手写generator

9.Promise

(1)Promise是ES6中新增的语法,用来解决回调地狱问题

  • 什么是回调地狱?

(2)手写Promise

10. == 和 === 的区别

==只需要值相等即可,而 === 需要值和类型都相等


11. 垃圾回收

主要的两种标记策略:标记清理引用计数

(1)标记清理

JS最常用的垃圾回收策略就是标记清理。

标记方法可以有多种:

  • 可以当变量进入上下文中,反转某一位
  • 也可以维护“在上下文中”和“不在上下文中”这样的两个变量列表,可以把变量从一个列表移到另一个列表
  • 其它

垃圾回收程序运行时,会标记内存中存储的所有变量,然后,它会将所有上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是有待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

(2)引用计数

另一种没那么常用的策略就是引用计数。

引用计数存在解决不了循环引用的问题

12.闭包

(1)定义

函数A返回函数B,函数B中使用了函数A中的变量,函数B就被称为闭包。

        function A() {
            let age = 10;
            return function B() {
                console.log(age);
            }
        }
        A()();

上面这个例子age变量是存储在堆上的,因为JS的引擎可以分析出哪些变量需要存储在堆上,哪些变量需要存储在栈上。

通过闭包解决变量作用域的问题,例如:解决var没有块作用域的问题

        for (var i = 0; i < 4; i++) {
            (function (j) {
                setTimeout(function () {
                    console.log(j);
                })
            }
            )(i)
        }

(2)闭包的内存泄漏问题

  • 使用JS闭包很容易在不知不觉间造成内存泄漏问题
        let outer = function () {
            let name = 'Jake';
            return function () {
                return name;
            }
        }

上面这段代码调用outer()会导致分配给name的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理name,因为闭包一直在引用着它,假如name的内容很大,那可能就是个大问题。

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

13.基本类型和引用类型

基础类型存储在栈上,引用类型的地址存储在栈上,内容存储在堆上

14.Eventloop

(1)为什么JS是单线程的

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则的话就会出现很多复杂的同步问题。假设JS有两个线程,一个线程往某个dom节点上添加内容,一个线程删除这个节点,那么应该以哪个线程为主呢,所以JS是单线程的就不会出现这种情况。

JS并发事件模型

(1)什么是Eventloop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

(2)微任务和宏任务

15.回调函数

16.setTimeout

(1)setTimeout倒计时误差

为什么会出现这个问题呢,从刚才上面的JS事件循环机制我们就可以了解到,只有等同步任务全部执行完了才会执行异步任务,而setTimeout是一个宏任务,那么这个问题的实际应用在哪呢,电商的秒杀倒计时,那个肯定要准确,不同用户可能因为手机的性能加载页面的时长不一样,但是秒杀时间肯定得一样,所以说我们就需要写一个相对准确的倒计时了。

setTimeout(() => {}, 3000);

有一行上面这样的代码,这不是说页面打开3秒后立即执行回调函数,而是页面打开后最快3秒执行回调函数。

        console.log(Date());
        setTimeout(() => {
            console.log('我是定时器!');
            console.log(Date());
        }, 3000);
        for (let i = 0; i < 10000000000; i++) { }

如果你电脑性能好的话可以跑下上面这个例子,你会发现间隔时间并不是3秒

看我这打印的结果,中间差了14秒,这是为什么呢,因为执行前面的循环花费了很长的时间,setTimeout必须等循环执行完才会去执行自己的回调函数,所以这时候这个倒计时就不准确了,就需要我们自己写一个倒计时

        let period = 60 * 1000 * 60 * 2,
            startTime = new Date().getTime(),
            count = 0,
            end = new Date().getTime() + period,
            interval = 1000,
            currentInterval = interval;
        function loop() {
            count++;
            let offset = new Date().getTime() - (startTime + count * interval),
                diff = end - new Date().getTime(),
                h = Math.floor(diff / (60 * 1000 * 60)),
                hdiff = diff % (60 * 1000 * 60),
                m = Math.floor(hdiff / (60 * 1000)),
                mdiff = hdiff % (60 * 1000),
                s = mdiff / (1000),
                sCeil = Math.ceil(s),
                sFloor = Math.floor(s),
                currentInterval = interval - offset;
            console.log('时:' + h, '分:' + m, '毫秒:' + s,
                '秒向上取整:' + sCeil, '代码执行时间:' + offset,
                '下次循环间隔' + currentInterval)
            // 打印 时 分 秒 代码执行时间 下次循环间隔
            setTimeout(loop, currentInterval)
        }
        loop();

17.防抖节流

18.数组降维

MDN参考

(1)使用 ES10 中的 flat()

        let arr = [50, 20, [50, 10, [1, [5, [20]]]]],
            arr1 = arr.flat(Infinity);
        console.log(arr); // [50, 20, Array(3)]
        console.log(arr1); //  [50, 20, 50, 10, 1, 5, 20]

(2)递归

        let arr = [50, 20, [50, 10, [1, [5, [20]]]]],
            resArray = [];
        function flatten(array) {
            if (Array.isArray(array) === false) {
                throw new TypeError('参数类型错误');
            }
            array.forEach(item => {
                if (Array.isArray(item)) {
                    flatten(item);
                } else {
                    resArray.push(item)
                }
            })
            return resArray;
        }
        console.log(arr); // [50, 20, Array(3)]
        console.log(flatten(arr)); // [50, 20, 50, 10, 1, 5, 20]

(3)