ZuoZuomu

仰望星空,也得脚踏实地。

后台
文章详情

js基础补充

2018-8-30

注意 new 运算符的优先级

function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName();   // -> 1
new Foo().getName(); // -> 2       

通过这段代码可以看出:new Foo() 的优先级高于 new Foo.

对于代码1 来说:是将 Foo.getName 当成一个构造函数来执行,执行构造函数所以输出为1.

对于代码2来说:通过 new Foo() 创建了一个Foo的实例,通过实例访问其原型链上的 方法所以输出为2.

注意非匿名的立即执行函数

var foo = 1;

// 有名立即执行函数
(function foo(){
    foo = 1;
    console.log(foo);
})();

// 执行这段代码会输出什么呢?

// -> ƒ foo() { foo = 10 ; console.log(foo) }

// 再去访问 foo 的值
foo
// -> 1

当JS执行器遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此行数内部才可以访问到 foo ,但这个值是只读的,所以对其重新赋值不会生效,所以打印结果还是这个函数,并且外部的值也没有发生改变。

关于对象的深拷贝

  • 可以使用 JSON.stringifyJSON.parse 这个两个方法

    优点:简单

    缺点:会忽略掉 undefined ; 不能序列化函数 ; 不能解决循环引用的对象

    function clone(obj) {
       return JSON.parse(JSON.stringify(obj));
    }
    
  • 使用递归循环赋值的方式

    优点:可以处理 undefined、函数等各种情况

    缺点:实现相对麻烦,效率不高

    function clone(obj) {
        if(!obj || typeof obj !== 'object') {
            return;
        }
        var _obj = obj.constructor === Object ? {} : [];
        for(let key in obj) {
            if(typeof obj[key] === 'object') {
                _obj[key] = clone(obj[key]);
            } else {
                _obj[key]  = obj[key];
            }
        }
        return _obj;
    }
    
    // 或者
    
    function clone(obj) {
        if(!obj || typeof obj !== 'object')
            throw new TypeError('params typeError');
        let _obj = obj.constructor === Object ? {} : [];
        Object.getOwnPropertyNames(obj).forEach(name => {
            if(typeof obj[name] === 'object') {
                _obj[name] = clone(obj[name]);
            } else {
                _obj[name] = obj[name];
            }
        });
        return _obj;
    }
    
  • 使用内置 MessageChannel 对象

    优点:是内置函数中处理深拷贝性能最快的

    缺点:不能处理函数(会报错)

    function clone(obj) {
        return new Promise(resolve => {
            let {port1, port2} = new MessageChannel();
            port2.onmessage = ev => resolve(ev.data);
            port1.postMessage(obj);
        });    
    }
    
    clone(obj).then(console.log);
    

关于async/await , promise 异步执行顺序

想解决这个问题,就必须知道 `await` 做了什么?

刚开始以为 await 会一直等待表达执行的执行结果,之后才会执行后面的代码。实际上 await 是一个让出线程的标志(遇到 await 会立即返回一个 padding 状态的promise)。await后面的函数会先执行一遍,然后就会跳出整个 async 函数来执行后面js代码。等本轮事件循环执行完又跳回到 async 函数中等待await后面表达式的返回值,如果返回值为非 promise 则继续执行async后面的代码,否则将 promse 加入队列。

且看一道面试题(分析代码执行 顺序):

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}

async function async2() {
   console.log("async2");
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});

console.log("script end");

OK,那接下来具体分析执行过程:

首先输出 "script start" ,然后立即将定时器加入异步事件队列。执行 async1() ,输出 "async1 start" ,进入 async2() ,输出 "async2" ,跳出整个 async1() 函数来执行后面js代码,执行promise执行器中的内容,输出 "promise1" ,执行resolve()回调,将then函数中的内容加入异步事件队列,接着输出 "script end" 。回到 async1() 的await等待 async2() 函数的返回值,因为返回值是一个promise实例,将promise加入异步事件队列。此时的同步代码执行完毕,轮询并从队列拿出代码放入主线程执行,所以输出 "promise2" ,继续执行 async1() 中的后续内容,输出 "async1 end" ,最后取出定时器中的内容并执行,输出 "settimeout"

综上所述:

script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout

那么再看一个例子应该会简单很多:

function testFunc() {
    console.log("testFunc..."); // 2
    return "testFunc";
}

async function testAsync() {
    console.log("testAsync...");
    return Promise.resolve("hello testAsync");
}

async function foo() {
    console.log("test start..."); // 1
    const v1 = await testFunc();
    connsole.log('hello world.'); // 5
    console.log(v1);              // 6 testFunc
    const v2 = await testAsync();
    console.log(v2);              // 8 hello testAsync
}

foo();

var promise = new Promise(resolve => { 
    console.log("promise start..");  // 3
    resolve("promise"); 
});
promise.then(val => console.log(val)); // 7 promise

console.log("test end..."); // 4

防抖和节流

  • 防抖:如果用户多次调用且间隔小于wait值,那么就会被转化为一次调用。
  • 节流:每隔一定时间(wait)调用函数 。

一个简单的防抖函数:

function debounce(func, wait) {
    let timer = null;
    return function(...params) {
        // 如果定时器存在则清除
        if(timer){
            clearTimeout(timer);
        }
        // 重新开始定时执行
        timer = setTimeout(() => {
            func.apply(this, params);
        }, wait);
    }
}

缺点:只能在最后执行,不能立即被执行,在某些情况下不适用。

改进...

function debounce(func, wait, immediate) {

    let timer, context, args;

    // 定时器
    let later = function() {
        return setTimeout(() => {
            timer = null;
            if(!immediate) {
                func.apply(context, args);
            }
        }, wait);
    }

    return function(...params) {
        if(!timer) {
            timer = later();

            // immediate 为 true,则立即执行
            // 否则 缓存上下文 和 参数
            if(immediate) {
                func.apply(this, params);
            } else {
                context = this;
                args = params;
            }
        } else {
            clearTimeout(timer);
            timer = later();
        }           
    }
}

call、apply和bind

怎么去模拟一个call函数呢?

思路:call一个非常重要的作用就是改变上下文环境也就是this,我们可以给用户传入的上下文对象上添加一个函数,通过这个上下文对象去执行函数,然后将这个函数删除,返回结果就可以了。

Function.prototype.myCall = function(context, ...args) {
    context = context || window;
    // 给上下文对象上添加这个函数
    context.fn = this;
    // 通过这个上下文对象去执行函数
    let result = context.fn(...args);
    // 将这个函数删除
    delete  context.fn;
    return result;
}

call既然都实现了,那么apply也是类似的,只不过传入的参数是一个数组而已。

Function.prototype.myApply = function(context, arr) {
    context = context || window;
    arr = arr || [];
    let type = {}.toString.call(arr).slice(8,-1);
    if(type !== 'Array')
        throw new TypeError('CreateListFromArrayLike called on non-object');
    context.fn = this;
    let result = context.fn(...arr);
    delete context.fn;
    return result;
}

模拟bind函数,bind函数应该返回一个新的函数。

Function.prototype.myBind  = function(context, ...args) {        
    // 保存当前的函数
    let func = this;
    return function F(...params) {
        if(this instanceof F) {
            return new func(...args, ...params);
        }
        return func.apply(context,[...args,...params]);
    }    
}

数组降维

function flattenDeep(arr) {
    if(!Array.isArray(arr))
        return [arr];
    return arr.reduce((prev,cur) => {        
        return [...prev, ...flattenDeep(cur)];
    },[]);
}

flattenDeep([1, [[2], [3, [4]], 5]]);
保持简单
秘钥: