[前端开发]《前端开发面试专题JS篇》

2019-05-13  本文已影响0人  杨山炮

1 原生JS面试专题

1.0 new一个对象做了什么

//https://www.jb51.net/article/137370.htm
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);

1.1 作用域,作用域链,上下文(this)

作用域 就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和局部作用域。作用域一般由以下两方面组成:

生成作用域的语法:

//函数作用域
function fnScope(){}
//异常捕获作用域
try{}catch(eeror){}
//ES6块级作用域
if(true){ let a=1,const b=2}

作用域链 当代码进入到一个执行环境中执行时,会创建变量对象的一个作用域链(scope chain,不简称sc)来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。作用域链其实就是引用了当前执行环境的变量对象的指针列表,它只是引用,但不是包含

this 函数中this的指向,是在函数被调用的时候确定的,也就是函数执行上下文被创建时确定的在函数执行时,this一旦被确定,就不可更改了。上下文,我把它理解为当前运行环境,程序运行时,程序的每条语句都有对应的上下文,即运行环境。

作用域和执行上下文
作用域和作用域链

1.2 闭包

定义: 闭包就是能够读取其他函数内部变量的函数,所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

原理:后台执行环境中,闭包的作用域链包含着自己的作用域、函数的作用域和全局作用域。 通常,函数的作用域和变量会在函数执行结束后销毁。 但是,当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止

特性

使用场景使用场景是基于闭包的闭包可以读取函数内部的变量,可以让变量的值始终保持在内存中这两个特点进行的拓展

//全局的变量
var datamodel = {    
    table : [],    
    tree : {}    
};    
//闭包
(function(dm){    
    for(var i = 0; i < dm.table.rows; i++){    
       var row = dm.table.rows[i];    
       for(var j = 0; j < row.cells; i++){    
           drawCell(i, j);    
       }    
    }     
    //build dm.tree      
})(datamodel);   

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象

var CacheManager=(function(){
        var cacheObj={};
        return {
            setItem:function(key,value){
                cacheObj[key]=value;
            },
            getItem:function(key){
                return cacheObj[key];
            },
            removeItem:function(key){
                delete cacheObj[key];
            },
            //清空缓存
            clear:function(){
                cacheObj={};
            }
        }
 })();
var person = function(){    
    //变量作用域为函数内部,外部无法访问    
    var name = "default";       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();       
console.log(person.name);//直接访问,结果为undefined    
console.log(person.getName());  // default   
person.setName("Tom");    
console.log(person.getName());    // Tom
//在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问
function Person(){    
    var name = "Tom";           
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
};        
var john = Person();    
console.log(john.getName());    // Tom
john.setName("Jack");    
console.log(john.getName());    // Jack
     
var jack = Person();    
console.log(jack.getName());    // Tom
jack.setName("Ricahrd");    
console.log(jack.getName());    // jack
//都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的

注意事项

1.3 JS创建对象的几种方式

1.3.0 工厂模式
function createPerson(name,age,jog){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job
  o.sayName = function(){
    console.log(this.name)
  }
  reurn this;
}

解决了创建多个对象的问题,但是无法解决对象从属问题

1.3.1 构造函数模式
function Person(name,age,job){
    this.name = name ;
    this.age = age;
    this.job = job;
    this.sayName = function(){console.log(this.name)};
    this.sayName = new Function(){console.log(this.name)}//等价于

}
p  = new Person("yxl",24,"worker")

new 方式创建对象经理以下四步骤

创建自定义的构造函数意味着可以将它标示为一个特定的类型,解决了对象从属问题,但是不同实例上的同名函数不相等会造成不同作用域和标识符解析。如果将sayName放到全局作用域中,那么这个函数只是被特定的对象调用,和方法的全局不相符。

1.3.2 原型模式
//第一种原型模式
function Person(name,age,job){
}
Person.prototype.name = "richard";
Person.prototype.age = "25";
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
    console.log(this.name)
}
p1 = new Person();
p2 = new Person();
p1.sayName = p2.sayName //true
图解原型对象.jpg
只要创建一个构造函数就会获得一个prototye属性,该属性指向该构造函数的原型对象,默认情况下该原型对象会自动获得一个constructor构造函数属性,该属性指向prototype属性的拥有者A构造函数。当调用构造函数创建一个对象实例之后,该实例将包含一个指向构造函数原型对象的指针_proto_,
A.prototype isPrototypeOf(new A())//true
//第二种原型模式
function Person(){}

var f1 = new Person();

Person.prototype = {
  constructor :"Person",
  name:"Richard",
  age:25,
  job:"Engineer",
  sayName:function(){
      console.log(this.name)
  }
}
var f2 = new Person();

f1.sayName()//Exception
f2.sayName()//Richard

上面f1和f2实例的不同输出结果显示重写Person的原型对象,切断了现有原型和任何之前已经存在的的对象实例之间的联系,f1实例引用的任然是最初的原型,而f2引用的是重写后的原型对象

原型模式创建对象会造成对象无法传参和引用类型数据共享的问题

1.3.3 构造函数模式+原型模式

function Person(name,age,job){
    this.name = name ;
    this.age = age ;
    this.job = job;
    this.friend = ["Tom","Richard"];
}
Person.prototype = {
    constructor:Person,
    sayName:function(){
        console.log(this.name)
    }
}

上述模式解决传参和引用类型数据共享问题,是目前比较常用的创建对象的方式

1.3.4 动态原型模式
function Person(name,age,job){
    this.name = name ;
    this.age = age ;
    this.job = job;
    this.friend = ["Tom","Richard"];
    if(typeof this.sayName!="function"){
        Person.prototype.sayName:function(){
            console.log(Date.now())
        }
    }
}
var p1 = new  Person("yang",34,"engineer");
var p2 = new Person("xing",54,"Famer");
p1.sayName()==p2.sayName()//true

动态原型模式将组合模式的构造函数和原型分开的做法进行合并,并且通过构造函数初始化了原型。

使用动态原型模式不能使用对象字面量重写原型

1.4 JS继承的几种方式

1.4.0 属性拷贝

略。。。

1.4.1 原型式继承
function createObj(o){
  function F(){}
  F.prototype = o;
  return new F()
}

在没有必要创建构造函数的情况下,仅仅是想一个对象与另一个对象保持类似,可以选用原型式继承。但当这个对象O包含引用类型数据的时候,会存在数据共享问题。ES5的Object.create(基础对象属性描述符)方法的规范了原型式继承

1.4.2 原型链继承

子构造函数.prototype = new 父构造函数();

// 创建父构造函数
function SuperClass(){
    this.name = 'richard';
    this.age = 25;
   this.friends = ['小名', '小强'];
    this.showName = function(){
        console.log(this.name);
    }
}
// 创建子构造函数
function SubClass(){
}
// 实现继承
SubClass.prototype = new SuperClass();
// 修改子构造函数的原型的构造器属性
SubClass.prototype.constructor = SubClass;
var child = new SubClass();
console.log(child.name); // richard
child.showName();//richard

原型链继承缺陷:无法为父级构造函数传递参数,父子构造函数之间存在数据共享问题

1.4.3 借用构造函数

使用call和apply借用其他构造函数的成员, 可以解决给父构造函数传递参数的问题, 但是获取不到父构造函数原型上的成员.也不存在共享问题.

function ParentClass(name){
    this.name = name;
}
function ChildClass(name,age){
    ParentClass.call(this,name)
    this.age = age;
}

通过这个方式的继承子类是无法获取到父类原型中的方法和属性的,同时方法都定义在构造函数中,所以函数复用就无从谈起

1.4.4 组合继承
function SuperType(name){
    this.name = name;
    this.friends = ["Tom","Richrad"]
}
SuperType.prototype.sayName = function(){
    console.log(this.name)
}

function SubType(name,age){
    SuperType.call(this,name);//第二次调用父类型,获取父类型对象的全部实例属性
    this.age = age;
}
SubType.prototype = new SuperType();//第一次调用父类型
// InheritPrototype(SubType,SuperType)寄生组合式继承
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);  
}
var instance1 = new SubType("Long",26);
instance1.friends.push("Yang");
instance1.sayName();
instance1.sayAge();

1.4.5 寄生组合式继承

function InheritPrototype(SubType,SuperType){
    var Supprototype = createObj(SuperType.prototype);
         Supprototype.constructor = SubType;
         SubType.prototype = Supprototype;
}

前面提到的组合式继承是当前首选的继承方式,但是会造成两次父构造函数的调用问题,一次是创建子类原型的时候,一次是在子类型内部.。
所谓寄生组合式继承即通过构造函数继承属性,通过原型链的混成继承方法,其实就是通过寄生式继承继承超类原型,然后将结果赋值给子类原型

//.prototype = new SuperType();//第一次调用父类型
//SubType.prototype.constructor = SubType;
InheritPrototype(SubType,SuperType);
//用上面的函数替换掉组合式继承的上面两行即可避免调用一次父类构造函数

1.5 for ... of,for in ,forEach, map 的区别

1.6 变量声明提升

变量提升
变量提升即将变量声明提升到它所在作用域的最开始的部分

window.onload = function(){
//1.
console.log(a)    Uncaught ReferenceError: a is not defined
//2.
console.log(a);    undefined
var a
//3. 
console.log(a);    undefined
var a=1
}

第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)

变量提升.png
函数声明和函数表达式
//函数声明
console.log(Fn)  //ƒ Fn(){}
function Fn(){}

//函数表达式
console.log(Fn) //undefiend 此时Fn是一个变量,结果和上面的提升结果一样
var Fn = function(){}1.11 几种常见异步编程方案

1.7 深拷贝和浅拷贝

浅拷贝:将原对象或原数组的引用地址拷贝给新对象或新数组

//赋值拷贝
let a = [1,2,4];
let b = a;

//原生JS实现浅拷贝
function shadowClone(obj){
    let shadowObj = Array.isArray?[]:{};
    if(obj&& typeof == "object"){
        for(key in obj){
            shadowObj[key] = obj[key]
        }
    }
}
//针对数组的浅拷贝
let a = [1,2,3];
let b = a.slice(0)||a.concat()

深拷贝:将原对象或数组的属性值拷贝给新对象或新数组

//原生JS实现深拷贝
function deepClone(obj){
//Object.prototype.toString.call(obj).toLowerCase().indexOf("array")
    if(obj ==null) return null;
    if(typeof obj!="object")return obj;
    //if(obj.constructor === Date) return new Date(obj);
    //if(obj.constructor === RegExp) return new RegExp(obj);
   // let deepObj = new obj.constructor();//保持继承链
    let deepObj = Array.isArray(obj)?[]:{};
    if(obj && typeof obj =="object" && obj.hasOwnProperty(key)){
        for(key in obj){
            if(obj[key] && typeof obj[key] =="object"){
                    deepObj[key] = arguments.callee(obj[key])
            }else{
                    deepObj[key] = obj[key]
            }
        }
    }
    return deepObj;
}
//JQ中实现深拷贝的方法
$.extend( [deep ], target, object1 [, objectN ] )
//Lodash方法库中的深拷贝
Lodash的deepClone方法
//下面的方法会把undefined、symbol、function 类型直接滤掉了
JSON.parse和JSON.stringry

参考资源:
尬谈Js对象的深拷贝与浅拷贝
深拷贝与浅拷贝的区别,实现深拷贝的几种方法

1.8 函数绑定bind实现与解析

if(!Function.prototype.bind){
    Function.prototype.bind = function(oThis){
        if(typeof this !== 'function'){
            throw new TypeError('被绑定的对象需要是函数')
        }
        var self = this//这里的this指向调用bind方法的函数对象
        var args = [].slice.call(arguments, 1)//这里的argument是bind时赋值的参数
        fBound = function(){ 
           //this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用,这里的this指向fBound作用在的对象(window|fBound实例)
           //当作为new 构造函数调用时会在内部自动生成一个空对象,并将空对象的原型指向构造函数func/fBound的prototype
            return self.apply(this instanceof fBound ? this : OThis, args.concat([].slice.call(arguments)))//这里的arguments指向的是fBoun调用时候赋值的参数
        }
        var func = function(){}
        //维护原型关系
        if(this.prototype){
          // 这里的this指向bind前面的函数对象
            func.prototype = this.prototype
        }
        //使fBound.prototype是func的实例,返回的fBound若作为new的构造函数,新对象的__proto__就是func的实例
        fBound.prototype = new func()
//上面两行相当于fBound.prototype = Object.create(this.prototype);
        return fBound
    }
}

代码文字解读

1.9 Promise实现

/**
 * Promise 实现 遵循promise/A+规范
 * Promise/A+规范译文:
 * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
 */

// promise 三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let that = this; // 缓存当前promise实例对象
    that.status = PENDING; // 初始状态
    that.value = undefined; // fulfilled状态时 返回的信息
    that.reason = undefined; // rejected状态时 拒绝的原因
    that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
    that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数

    function resolve(value) { // value成功态时接收的终值
        if(value instanceof Promise) {
            return value.then(resolve, reject);
        }

        // 为什么resolve 加setTimeout?
        // 2.2.4规范 onFulfilled 和 onRejected 只允许在 execution context 栈仅包含平台代码时运行.
        // 注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 
        // 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

        setTimeout(() => {
            // 调用resolve 回调对应onFulfilled函数
            if (that.status === PENDING) {
                // 只能由pedning状态 => fulfilled状态 (避免调用多次resolve reject)
                that.status = FULFILLED;
                that.value = value;
                that.onFulfilledCallbacks.forEach(cb => cb(that.value));
            }
        });
    }

    function reject(reason) { // reason失败态时接收的拒因
        setTimeout(() => {
            // 调用reject 回调对应onRejected函数
            if (that.status === PENDING) {
                // 只能由pedning状态 => rejected状态 (避免调用多次resolve reject)
                that.status = REJECTED;
                that.reason = reason;
                that.onRejectedCallbacks.forEach(cb => cb(that.reason));
            }
        });
    }

    // 捕获在excutor执行器中抛出的异常
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

/**
 * resolve中的值几种情况:
 * 1.普通值
 * 2.promise对象
 * 3.thenable对象/函数
 */

/**
 * 对resolve 进行改造增强 针对resolve中不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {  // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
        return reject(new TypeError('循环引用'));
    }

    let called = false; // 避免多次调用
    // 如果x是一个promise对象 (该判断和下面 判断是不是thenable对象重复 所以可有可无)
    if (x instanceof Promise) { // 获得它的终值 继续resolve
        if (x.status === PENDING) { // 如果为等待态需等待直至 x 被执行或拒绝 并解析y值
            x.then(y => {
                resolvePromise(promise2, y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        } else { // 如果 x 已经处于执行态/拒绝态(值已经被解析为普通值),用相同的值执行传递下去 promise
            x.then(resolve, reject);
        }
        // 如果 x 为对象或者函数
    } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是否是thenable对象(具有then方法的对象/函数)
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, reason => {
                    if(called) return;
                    called = true;
                    reject(reason);
                })
            } else { // 说明是一个普通对象/函数
                resolve(x);
            }
        } catch(e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

/**
 * [注册fulfilled状态/rejected状态对应的回调函数]
 * @param  {function} onFulfilled fulfilled状态时 执行的函数
 * @param  {function} onRejected  rejected状态时 执行的函数
 * @return {function} newPromsie  返回一个新的promise对象
 */
Promise.prototype.then = function(onFulfilled, onRejected) {
    const that = this;
    let newPromise;
    // 处理参数默认值 保证参数后续能够继续执行
    onFulfilled =
        typeof onFulfilled === "function" ? onFulfilled : value => value;
    onRejected =
        typeof onRejected === "function" ? onRejected : reason => {
            throw reason;
        };

    // then里面的FULFILLED/REJECTED状态时 为什么要加setTimeout ?
    // 原因:
    // 其一 2.2.4规范 要确保 onFulfilled 和 onRejected 方法异步执行(且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行) 所以要在resolve里加上setTimeout
    // 其二 2.2.6规范 对于一个promise,它的then方法可以调用多次.(当在其他程序中多次调用同一个promise的then时 由于之前状态已经为FULFILLED/REJECTED状态,则会走的下面逻辑),所以要确保为FULFILLED/REJECTED状态后 也要异步执行onFulfilled/onRejected

    // 其二 2.2.6规范 也是resolve函数里加setTimeout的原因
    // 总之都是 让then方法异步执行 也就是确保onFulfilled/onRejected异步执行

    // 如下面这种情景 多次调用p1.then
    // p1.then((value) => { // 此时p1.status 由pedding状态 => fulfilled状态
    //     console.log(value); // resolve
    //     // console.log(p1.status); // fulfilled
    //     p1.then(value => { // 再次p1.then 这时已经为fulfilled状态 走的是fulfilled状态判断里的逻辑 所以我们也要确保判断里面onFuilled异步执行
    //         console.log(value); // 'resolve'
    //     });
    //     console.log('当前执行栈中同步代码');
    // })
    // console.log('全局执行栈中同步代码');
    //

    if (that.status === FULFILLED) { // 成功态
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
                } catch(e) {
                    reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
                }
            });
        })
    }

    if (that.status === REJECTED) { // 失败态
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

    if (that.status === PENDING) { // 等待态
        // 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
        return newPromise = new Promise((resolve, reject) => {
            that.onFulfilledCallbacks.push((value) => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
            that.onRejectedCallbacks.push((reason) => {
                try {
                    let x = onRejected(reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
};

/**
 * Promise.all Promise进行并行处理
 * 参数: promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
 */
Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
            }, reject)
        })
    })
}

function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function(i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}

/**
 * Promise.race
 * 参数: 接收 promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
 */
Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}

// 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

/**
 * 基于Promise实现Deferred的
 * Deferred和Promise的关系
 * - Deferred 拥有 Promise
 * - Deferred 具备对 Promise的状态进行操作的特权方法(resolve reject)
 *
 *参考jQuery.Deferred
 *url: http://api.jquery.com/category/deferred-object/
 */
Promise.deferred = function() { // 延迟对象
    let defer = {};
    defer.promise = new Promise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}

/**
 * Promise/A+规范测试
 * npm i -g promises-aplus-tests
 * promises-aplus-tests Promise.js
 */

try {
  module.exports = Promise
} catch (e) {
}

1.10 数组去重

ES6的数组去重,利用Set对象的元素不重复的特点

//eg1:
[...new Set(arr)]
//eg2:
function distinct(arr) {
  return Array.from(new Set(arr))
}
//includes:{}无法去重
function distinct(arr) {
    if (!Array.isArray(arr)) return ;
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}

ES5中提供的去重的API

//filter
function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
//indexOf:NaN、{}类型无法去重
function distinct(arr) {
    if (!Array.isArray(arr)) return ;
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) === -1) {
            array.push(arr[i])
        }
    }
    return array;
}
//reduce
function distinct(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}

ES3中数组去重的API

//sort:NaN、{}类型无法去重
function Distinct_5(arr){
    if(!Array.isArray(arr))return ;
    arr = arr.sort()
    var array = [arr[0]];
    for(var i=1;i<arr.length;i++){
        if(arr[i]!=arr[i-1]){
            array.push(arr[i])
        }
    }
    return array;
}
//splice:NaN和{}没有去重,null类型直接消失
function distinct(arr){            
      var len = arr.length;
        for(var i=0; i<len ; i++){
            for(var j=i+1; j<len ; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                    len--;
                }
            }
        }
    return arr;
}

1.11 几种常见异步编程方案

1.11.0 回调函数
f1();
f2();
/**************************/
function f1(callback){
   setTimeout(function () {
    // f1的任务代码
     callback();
   }, 1000);
 }
f1(f2)

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
缺点:代码耦合,不利于维护

1.11.1 事件监听

异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

f1.on('done', f2);

当f1监听到done事件之后就自动执行f2

  function f1(){
    setTimeout(function () {
      // f1的任务代码
      f1.trigger('done');
    }, 1000);

  }

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化
缺点:是整个程序都要变成事件驱动型,运行流程会变得很不清晰

1.11.2 发布订阅模式
// 发布订阅(Publish/Subscribe)模式实现
var pubsub = {};
(function(q){
var topics = {},
    subUid = -1;
    // 发布或者广播事件,包含特定的topic和传递的数据
    q.publish = function(topic,args){
        if(!topics[topic]){
            return false
        }
        var subscribers = topics[topic],
            len = subscribers?subscribers.length:0;
        while(len--){
            subscribers[len].func(topic,args);
        }
        return this;
    },
    // 通过特定的名称和回调函数订阅事件,topic/event触发时执行事件
    q.subscribe = function(topic,func){
        if(!topics[topic]){
            topics[topic] = []
        }
        var token = (++subUid).toString();
        topics[topic].push({
            token:token,
            func:func
        });
        return token;
    },
    // 基于订阅山的标记引用,通过特定的topic取消订阅
    q.unsubscribe = funciton(token){
        for(var m in topics){
            if(topics[m]){
                for(var i=0;j<topics[m].length;i<j;i++){
                    topics[m].splice(i,1);
                    return token;
                }
            }
        }
        return this;
    }
})(pubsub)

我们假定,存在一个"信号中心"-pubsub,某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行

jQuery.subscribe("done", f2);
function f1(){
    setTimeout(function () {
      // f1的任务代码
      jQuery.publish("done");
    }, 1000);
  }

这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Javascript异步编程的4种方法-阮一峰

1.11.3 Promise/A+
promises.png
1.11.4 生成器Generators/ yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行,一般结合co 模块一起使用

function* foo(x) {
  var y = 2 * (yield (x + 1))
  var z = yield (y / 3)
  return (x + y + z)
}
var it = foo(5)//返回一个遍历器对象
//第一次调用next会忽略传入的参数,并且函数暂停在 yield (x + 1) 处,所以返回 5+1=6
console.log(it.next())   // => {value: 6, done: false}
//第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined,
//此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
console.log(it.next(12)) // => {value: 8, done: false}
//第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,
//所以 z = 13, x = 5, y = 24,相加等于 42
console.log(it.next(13)) // => {value: 42, done: true}
1.11. 5 async/await

只要函数名之前加上async关键字,就表明该函数内部有异步操作。该异步操作应该返回一个Promise对象,前面用await关键字注明。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
async function asyncValue(value) {
  await timeout(50);
  return value;
}

JS异步编程的几种方式

1.12 事件代理

“事件代理”即是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务,事件代理的原理是DOM元素的事件冒泡。

事件传播分成三个阶段:

/*html代码*/
    <input type="button" value="添加" id="input1" />
<ul id="#ul1">
    <li>100</li>
    <li>200</li>
    <li>300</li>
    <li>400</li>
</ul>
/*JS代码*/
window .onload = function(){
    var oUl  = document.getElementById('ul1');
    var oLi  = oUl.getElementById('li');
    var oBtn = document.getElementById('input1');
    var iNow = oLi.length;
          oUl.onmouseover = function(ev){
               var ev  = ev||window.event
               var target  = ev.target || ev.srcElement;
               if(target.nodeName.toLowerCase()=="li"){
                target.style.background = 'red';
                } 
          };
            oUl.onmouseout = function(ev){
               var ev  = ev||window.event
               var target  = ev.target || ev.srcElement;
               if(target.nodeName.toLowerCase()=="li"){
                target.style.background = '';
                } 
          };
          oBtn.onclick = function(){
                iNow++;
               var aLi  = createElement("li");
               aLi.innerHTML = 100*iNow;
               oUl.appendChild(aLi);
                } 
          };
} 

事件委托的好处:

1.13 单例模式实现一个前端存储对象storage

ES5方式实现单例模式

var ES5Singletone = function(){
    this.instance = null;
    this.data = {} ;
}
ES5Singletone.getInstance = function(){
    if(!this.instance){
        return this.instance = new ES5Singletone();
    }
    return this.instance;
}
ES5Singletone.prototype.getItem = function(key){
    return this.data[key]
}
ES5Singletone.prototype.setItem = function(key,val){
    this.data[key] = val;
}
var a = ES5Singletone.getInstance();
var b = ES5Singletone.getInstance();
console.log(a===b)//true
a.setItem("key",111111);
console.log(a.getItem("key"))

ES6实现Storage

class ES6Singleton {
    constructor() {
      this.instance = null;
      this.data = {};
    }
    static getInstance(){
        if(!this.instance){
            return this.instance = new ES6Singleton();
        }
        return this.instance;
    }
    getItem(name){
        return this.data[name]
    }
    setItem(key,val){
        this.data[key] = val;
    }
}
var c = ES6Singleton.getInstance();
var d = ES6Singleton.getInstance();
c.setItem("key2","22222");
d.setItem("key2","33333");
console.log(c.getItem("key2"));//3333 相同实例,数据共享

1.14 AJAX工作原理

在AJAX实际运行当中,对于访问XMLHttpRequest(XHR)时并不是一次完成的,而是分别经历了多种状态后取得的结果,对于这种状态在AJAX中共有5种

1.15 解析URL地址参数

const parseQueryString = url=>{
     var json = {};
     var arr = url.substr(url.indexOf('?') + 1).split('&');
    arr.forEach(item=>{
        var tmp = item.split('=');
            json[tmp[0]] = tmp[1];
    });
    return json;
}

//将POST请求参数字符串解析成JSON
const parsePostQuery = (str)=>{
    let parse2JsonObj  = {};
    let queryStrArr = str.substr(str.indexOf('?') + 1).split('&');

    // ["name=yyy","age=25","sex=nv"]
    // arr.entries()返回数组的迭代对象[0,"str1"],[1,"str2"]
    for(let [index,queryString] of queryStrArr.entries()){
        let item  = queryString.split("=");
        parse2JsonObj[item[0]] = decodeURIComponent(item[1]);
    }
    return parse2JsonObj;

};

1.16 数组全排列

var testArr = [1,2,3]
function fullSort(arr){
   var result = [];
   if (arr.length == 1) {
       result.push(arr);
       return result;
   }
   for (var i = 0; i < arr.length; i++) {
       var temp = []; 
       temp.push(arr[i]); //取任意一项放到temp的第一项
       var remain = arr.slice(0);//深复制原数组到remain
       remain.splice(i,1); //去掉那一项
       var temp2 = fullSort(remain).concat(); //为剩下项的全排列[[2,3],[3,2]]
       console.log(temp2,remain)
       for (var j = 0; j < temp2.length; j++) {
           temp2[j].unshift(temp[0]); // [[1,2,3],[1,3,2]]这样的数据
           result.push(temp2[j]);
       }
   }
   return result;
}
console.log(fullSort(testArr))

1.17 出现最多的字符或数字的解析,并统计次数

var str = "yxlyxlyxlllyyyyxxxllll"
function CountMaxStr(str){
  var value = null;
  var num= 0;
  var json = {}
for(var i=0;i<str.length;i++){
    if(!json[str[i]]){
       json[str[i]] = [];
   }else{
      json[str[i]].push(str[i])    
  }
}
for(attr in json){
    if(num<json[attr].length){
        num = json[attr].length;
        value = json[attr]
    }
}
}
//2
function max2(){
var json={},value=null,num=0;
    for(var i=0;i<str.length;i++){
        var k=str[i]; //k是所有字符,字符串也跟数组一样可以通过中括号下标方法取到每个子元素
        if(json[k]){
            json[k]++; //json里有这个字符时,把这个字符的数量+1,
        }else{
            json[k]=1; //否则把这个字符的数量设为1
        }
    }
    for(var k in json){ 
        if(json[k]>num){
            num=json[k];
            value=k;
        }
    }
console.log(json)
console.log("出现最多的字符是:"+value+',出现次数是:'+num);
};
//3
function max3(){
    var new_str=str.split("").sort().join("");
    var re=/(\w)\1+/g; //没有\1,re就是一整个排好序的字符串,有了\1就是出现过的有重复的取出来,\1表示跟前面第一个子项是相同的
    new_str.replace(re,function($0,$1){ //$0代表取出来重复的一个个整体,如[s,s...],[f,f..],[d,d....] $1代表这个整体里的字符
        console.log($0,$1)
        if(num<$0.length){
            num=$0.length;
            value=$1
        }
    });
console.log(value+":"+num)
}

1.18 去空格

// str为要去除空格的字符串:
// 去除所有空格:   
str   =   str.replace(/\s+/g,"");       
// 去除两头空格:   
str   =   str.replace(/^\s+|\s+$/g,"");
// 去除左空格:
str=str.replace( /^\s*/, '');
// 去除右空格:
str=str.replace(/(\s*$)/g, "");
String.prototype.trim = function(str){
 return  str.replace(/\s+/g,"");       
}

1.19 降维数组

//1
function dropDownArray_1(arr){
   let array = [];
   for(var i=0;i<arr.length;i++){
       array = array.concat(arr);
   }
   return array
}
//2
function dropDownArray_2(arr){
   return Array.prototype.concat.apply([],arr);
}
//3
Array.prototype.dropDownArray_3 = function(){
   var result = [];
   this.forEach((val,index)=>{
       if(Array.isArray(val)){
           val.forEach(arguments.callee)
       }else{
           result.push(val)
       }
   })
   return result;
}

1.20 ES6和ES5的类有什么区别

1.21 回文字符串判断

function isPalindrome(str){
  var len = Math.floor(str.length/2);
  for(var i=0;i<len;i++){
      if(str.charAt(i)!=str.charAt(str.length-i-1)){
          console.log("not Palindrome")
          return false;
      }else{
          console.log("isPalindrome");
          return true;
      }
  }
}
var str = "abccba";
var str2 = "zxcvbbvcxzsd"
console.log(isPalindrome(str))
console.log(isPalindrome(str2))

//2
function palindrome (str) {
// 删除字符串中不必要的字符
var re = /[\W_]/g;
// 将字符串变成小写字符
var lowRegStr = str.toLowerCase().replace(re, '');
// 如果字符串lowRegStr的length长度为0时,字符串即是palindrome
if (lowRegStr.length === 0) {
   return true;
}
// 如果字符串的第一个和最后一个字符不相同,那么字符串就不是palindrome
 if (lowRegStr[0] !== lowRegStr[lowRegStr.length - 1]) {
     return false;
 } else {
     return palindrome(lowRegStr.slice(1, lowRegStr.length - 1));
 }
}

1.22 this指向

1.22.0 普通函数调用指向window对象
1.22.1 对象函数调用,谁调用指向谁
1.22.2 构造函数中的this指向构造函数实例

在构造函数里面返回一个对象,会直接返回这个对象,而不是执行构造函数后创建的对象

1.22.3 apply和call中的this,改变前面函数的执行环境
1.22.4 箭头函数里面的 this 是继承外面的环境。

  1. 箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
  2. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替
  3. 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  4. 箭头函数不能作为构造函数,和 new 一起用就会抛出错误
  5. 箭头函数没有原型属性
  6. 不能简单返回对象字面量
let func = () => { foo: 1 };
console.log(func());  //undefined

//如果要返回对象字面量,用括号包裹字面量
let func = () => ({ foo: 1 });
console.log(func());   //{ foo: 1 }

1.22.5 定时器中的this指向window(非箭头函数情况下)

1.23 函数防抖与节流

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间

function debounce(func,wait,immediate) {
    let timeout;
    return function () {
        let context = this;//调用匿名函数的对象
        let args = arguments;//事件对象
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

所谓节流,就是指对于连续触发的事件,在 n 秒中只执行一次函数,节流会稀释函数的执行频率。

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func,wait,type){
    var previous = 0, timer = null;
    return function(){
        var context = this;
        var args = arguments;
        if(type==1){
            var now = Date.now();
            if(now-previous>wait){
                previous = now ;
                func.apply(context,args);
            }
        }
        if(type ==2){
            if(!timer){
                timer = setTimeout(function(){
                    clearTimeout(timer);
                    func.apply(context,args);
                },wait)
            }
        }
    }
}

1.24 事件队列(EventLoop)

1.24.0 JS引擎执行模型

从宏观角度讲, js 的执行是单线程的. 所有的异步结果都是通过 “任务队列(Task Queue)” 来调度被调度. 消息队列中存放的是一个个的任务(Task). 规范中规定, Task 分为两大类, 分别是 Macro Task(宏任务) 和 Micro Task(微任务), 并且每个 Macro Task 结束后, 都要清空所有的 Micro Task.

宏观上讲, Macrotask 会进入 Macro Task Queue, Microtask 会进入 Micro Task Queue。而 Micro Task 被分到了两个队列中. ‘Micro Task Queue’ 存放 Promise 等 Microtask. 而 ‘Tick Task Queue’ 专门用于存放 process.nextTick 的任务.现在先来看看规范怎么做的分类.

‘每个 MacroTask 结束后, 都要清空所有的 Micro Task‘. 引擎会遍历 Macro Task Queue, 对于每个 MacroTask 执行完毕后都要遍历执行 Tick Task Queue 的所有任务, 紧接着再遍历 MicroTask Queue 的所有任务. (nextTick 会优于 Promise执行)

以上所说都是基于浏览器端的 EventLoop

eventloop.jpg

JS事件队列执行流(EventLoop)
JS引擎的执行机制:探究EventLoop(含Macro Task和Micro Task)
https://www.w3.org/TR/html5/webappapis.html#event-loops

1.25 函数式编程理解与应用场景

1.25.0 函数式编程理解

函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程

1.25.1 函数是一等公民

一等公民是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值

// 赋值
var a = function fn1() {  }
// 函数作为参数
function fn2(fn) {
    fn()
}   
// 函数作为返回值
function fn3() {
    return function() {}
}
1.25.2 纯函数

纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数,无副作用指的是函数内部的操作不会对外部产生影响(如修改全局变量的值、修改 dom 节点等)

1.25.3 函数柯里化

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法

    var curry = function(f) {
      var len = f.length;
        return function t() {
          var innerLength = arguments.length,
            args = Array.prototype.slice.call(arguments);
           console.log("innerLength:::",innerLength)
          if (innerLength >= len) {   
             return f.apply(undefined, args)
          } else {
            return function() {
              var innerArgs = Array.prototype.slice.call(arguments),
                allArgs = args.concat(innerArgs); 
              return t.apply(undefined, allArgs)
            }
          }
        }
    }
  // 一个参数
  function identity(value) {
     return value;
 }
   var curriedIdentify = curry(identity);
   console.log(curriedIdentify(4)) // 4
   // 多个参数
  function add(num1, num2) {
    return num1 + num2;
  }
   var curriedAdd = curry(add);
   console.log(curriedAdd(1))//[Function]
   console.log(curriedAdd(2)(3));  //5
   console.log(curriedAdd(4,5))  //9 
   console.log(curriedAdd(4,5,6))//9
var addEvent = (function() {
    if(window.addEventListener) {
        return function(ele, type, fn, isCapture) {
            ele.addEventListener(type, fn, isCapture)
        }
    } else if(window.attachEvent) {
        return function(ele, type, fn) {
             ele.attachEvent("on" + type, fn)
        }
    }
})()
1.25.4 组合函数
function compose() { //参数执行动作的顺序是从右往左
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        console.log("000",arguments)//000 { '0': 'ttsy' }
        var result = args[start].apply(this, arguments);
        console.log("1111",result)//1111 hello ttsy
        while (i--) result = args[i].call(this, result);
        console.log("2222",result)//2222 YSTT OLLEH
        return result;
    };
}

function addHello(str){
    return 'hello '+str;
}
function toUpperCase(str) {
    return str.toUpperCase();
}
function reverse(str){
    return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy'));  // YSTT OLLEH

组合函数是将多个函数的能力合并,创造一个新的函数。上面的函数可以针对对个函数进行组合,执行的顺序是函数性的参数从右自左执行

1.25.5 高阶函数

接受或者返回一个函数的函数称为高阶函数,我们经常可以在 JavaScript 中见到许多原生的高阶函数,例如 Array.map , Array.reduce , Array.filter
满足下列条件之一的函数就可以称为高阶函数。
1、函数作为参数被传递;
2、函数作为返回值输出。

1.25.6 Point Free

把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。

1.25.7 声明式与命令式代码

我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示

//命令式
let CEOs = [];
for (var i = 0; i < companies.length; i++) {
    CEOs.push(companies[i].CEO)
}
//声明式
let CEOs = companies.map(c => c.CEO);

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可

1.25.8 函数式编程的优劣

参考文章:函数式编程

函数式编程
JavaScript函数式编程
阮一峰-函数式编程

1.26 requestAnimationFrame请求动画帧

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。

设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

requestAnimationFrame() 方法接收一个参数,即要执行的回调函数。这个回调函数会默认地传入一个参数,即从打开页面到回调函数被触发时的时间长度,单位为毫秒,精确度为10微秒。

var startTime;

function sayHi(time){
    console.log("11111==>",time)
    if(!startTime){
        startTime = time;
    }
    time -= startTime;
    console.log("2222====>",startTime);
    if (time <= 200) {
        requestAnimationFrame(sayHi);
    }
}

requestAnimationFrame(sayHi);
log.jpg

这个方法返回一个唯一的requestID,类似于定时器的的返回值,可以通过将这个标识符传给 cancleAnimationFrame() 方法来取消这个回调函数。

Note:显示器有固定的刷新频率(60Hz或75Hz,,也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步

阮一峰-requestAnimationFrame
容易被忽视的requestAnimationFrame

上一篇下一篇

猜你喜欢

热点阅读