SunXinFei / sunxinfei.github.io

前后端技术相关笔记,已迁移到 Issues 中
https://github.com/SunXinFei/sunxinfei.github.io/issues
32 stars 3 forks source link

前端基础 #21

Open SunXinFei opened 4 years ago

SunXinFei commented 4 years ago

异步与同步

单线程与多线程

js为什么是单线程?js语言在设计之初就是单线程的,原因其实也比较简单,js主要是针对浏览器使用,浏览器中单线程能够保证页面中DOM的渲染不出现异常,也正是这个原因,所以在现在新标准允许JavaScript脚本多线程中有一个严格的要求,子线程不能够进行DOM操作,来严格把控住,所以所谓js多线程,也就只能做一些业务上面的运算,比如数量非常大的数据清洗等等。

任务队列

Philip Roberts的PPT视频《Help, I'm stuck in an event-loop》中已经非常清晰地说明了关于任务队列的讲解:

  1. 所有任务在主线程执行,形成一个执行栈(execution context stack)
  2. 一旦执行栈(execution context stack)中没有了任务,便会去"任务队列"(task queue)中读取任务放置到执行栈(execution context stack)中去执行
  3. 不断重复

    宏任务&微任务

    image

宏任务一般包括:整体代码script,setTimeout,setInterval, setImmediate。 微任务:Promise,process.nextTick , Object.observe [已废弃], MutationObserver

(()=>{
  console.log('script start') //1

async function async1() {
  await async2()
  console.log('async1 end')//5
}
async function async2() {
  console.log('async2 end') //2
}
async1()

setTimeout(function() {
  console.log('setTimeout')//8
}, 0)

new Promise(resolve => {
  console.log('Promise')//3
  resolve()
})
  .then(function() {
    console.log('promise1')//6
  })
  .then(function() {
    console.log('promise2')//7
  })

console.log('script end')//4
}
)()

https://juejin.im/post/6844903764202094606

浏览器与node的区别

浏览器为: image node为: image 浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。

SettimeOut

基于上面的任务队列,settimeOut函数也就可以解释一些参数0之类的问题了,方法先执行主线程中执行栈的方法,settimeout中的方法会放置到任务队列,并且先进先出,执行栈执行完之后,去任务队列中拿出方法放入执行栈去执行,直到结束为止。 其中delay的毫秒参数的含义如下,MDN,里面提到一点很重要实际延迟可能比预期的更长;这一点很容易理解:实际延迟的时间是看该延迟函数方法之前的那些主线程执行栈中方法执行的时间以及队列中放入执行栈去执行的时间。如果两者超出了延迟时间,那么就产生了实际延迟可能比预期的更长的现象,下面是实验代码验证这个观点

delay Optional The time, in milliseconds (thousandths of a second), the timer should wait before the specified function or code is executed. If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, as soon as possible. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.

console.log(1);
setTimeout(function(){console.log(2);}, 0);
setTimeout(function(){console.log(3);}, 0);
console.log(5);
//1
//4
//函数返回undefined
//2 
//3
(()=>{
  console.log(1,Date.now());
  setTimeout(function(){console.log(2,Date.now() );}, 1000);
  setTimeout(function(){console.log(3,Date.now());}, 0);
  console.log(4,Date.now());
})()
//1 1564650928850
//4 1564650928850
//函数返回undefined
//3 1564650929023
//2 1564650929851

这个例子验证了一点,1000毫秒的含义,其实表示的是第一次运行到这里是1564650928851左右的时间,等到队列中的方法执行完之后,此时间隔时间如果超过了1000ms,则立即执行,如果低于1000ms,则去等待1000ms之后去执行。

(()=>{
    console.log(1, Date.now());
    setTimeout(function() {
        console.log(2, Date.now());
    }, 100);
    setTimeout(function() {
        console.log(3, Date.now());
    }, 0);
    console.log(4, Date.now());
}
)()
//1 1564651900863
//4 1564651900863
//函数返回undefined
//3 1564651901027
//2 1564651901027

这里就验证了,所谓的延迟100ms并不一定是相对延迟了100ms之后,就要去执行,实际延迟可能比预期的更长所以当队列执行到此时,时间已经延迟大于了100ms,所以就立即去执行log 2这个方法。

SunXinFei commented 4 years ago

js中的继承

我们先说下《JS高级程序设计》中提到的组合式继承;继承即为继承父类属性和继承父类方法,组合式继承核心是使用call继承属性;原型对象上挂载要继承的方法;故为代码如下:

//父类构造函数
function SuperType(name){
  this.name = name;
  this.colors = ['red','white']
}
SuperType.prototype.sayName = function(){
  console.log(this.name);
}
function SubType(name,age){
  this.age = age;
  SuperType.call(this, name);
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
  console.log(this.age);
}

let instance = new SubType('Nical',12);

image

缺点:父类构造函数执行了两次,SubType.prototype = new SuperType();会导致SubType.prototype挂载上父类的属性:name和colors,通过SuperType.call(this, name);在子类实例上挂载属性进行屏蔽。

寄生组合式继承

核心思想:复制一个父类的原型对象赋值给子类的prototype,不必为了指定子类的原型而调用超类构造函数。

/**
        * subType- 子类型构造函数
        * superType-超类型构造函数
        */
        function inheritPrototype(subType, superType) {
            //1.创建了超类(父类)原型的(副本)浅复制
            var prototype = Object.create(superType.prototype);

            /*
                2.修正子类原型的构造函数属性
                 constructor属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数
                prototype.constructor  未修改前指向的 superType,为了弥补因重写原型而失去的默认constructor属性。

            */
            prototype.constructor = subType;
            // 3.将子类的原型替换为超类(父类)原型的(副本)浅复制
            subType.prototype = prototype;
        }
function SuperType(name) {
            this.name = name;
            this.colors = ["red", "blue", "green"];
        }

        SuperType.prototype.sayName = function () {
            alert(this.name);
        };

        function SubType(name, age) {
            //构造函数式继承--子类构造函数中执行父类构造函数
            SuperType.call(this, name);
            this.age = age;
        }
        // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
        inheritPrototype(SubType, SuperType);

        SubType.prototype.sayAge = function () {
            alert(this.age);
        }
        var instance = new SubType("lichonglou");
        console.log(instance.name)
        // console.log(instance.constructor)//指向SubType 如果没有修正原型的构造函数,则会指向父类构造函数

image

SunXinFei commented 3 years ago

ECMA6设计模式

单例模式

class Singleton{
   constructor(){
     if(!this.constructor.instance){
       this.constructor.instance = this ;
     }
     return this.constructor.instance;
   }
 }

 const instance1 = new Singleton();
 const instance2 = new Singleton();
 console.log(instance1 === instance2);//true

image

发布订阅

class EventEmitter {
        constructor(){
            this._events = {};
        }
        on(event,callback){//事件绑定
            const callbacks = this._events[event] || [];
            callbacks.push(callback);
            this._events[event] = callbacks;
        }
        emit(...args){//事件触发
            const event = [].shift.call(args);//Array.prototype.shift.call(args); //args.shift();
            (this._events[event] || []).forEach(cb=>cb.apply(this,args))
        }
        off(event,callback){//事件解除
            const callbacks = (this._events[event] || []).filter(cb=> cb!==callback);
            this._events[event] = callbacks;
        }
        once(event,callback){//只触发一次,触发后解绑
            const wrap = (...args)=>{
                callback.apply(this,args);
                this.off(event,wrap);
            }
            this.on(event,wrap);
        }
    }
    //on&emit
    eventEmitter.on('abc', test);
    eventEmitter.emit('abc', 3, 666);//3 666
    //off
    eventEmitter.off('abc',test);
    //once
    eventEmitter.once('abc',test);
    eventEmitter.emit('abc',666,1);//666 1
    eventEmitter.emit('abc',888,1);//不输出
SunXinFei commented 3 years ago

依赖注入(Dependency Injection)控制反转(Inversion of Control)

提升开发效率 提高模块化 便于单元测试

SunXinFei commented 3 years ago

跨域options

W3C规范:跨域请求中,分为简单请求和复杂请求;所谓的简单请求,需要满足如下几个条件:

简单请求不会触发 CORS 预检请求(嗅探请求) 简单请求之外为复杂请求

SunXinFei commented 3 years ago

Promise

Promise规范

then

总的来说:then方法提供一个供自定义的回调函数,若传入非函数,则会忽略当前then方法,回调函数中会把上一个then中返回的值当做参数值供当前then方法调用。 then方法执行完毕后需要返回一个新的值给下一个then调用(没有返回值默认使用undefined)。 每个then只可能使用前一个then的返回值。 再次描述: 对于传入 then 方法的参数,首先判断其是否为 function,判断为否,直接执行 下一个 then 的 success 函数;判断为是,接着判断函数的返回值 res 类型是否为 Promise,如果为否,直接执行下一个 then 的 success 函数,如果为是,通过 then 调用接下来的函数。

实现

//Promise 支持异步
    class SimplePromise {
        constructor(callback) {
            this._value = null;//存储value值
            this.status = 'pending';
            this._onSuccess = [];
            this._onFail = [];
            let resolve = (val)=>{//传递给外部去主动调用的成功函数
                if (this.status === 'pending') {
                    this.status = 'resolve';
                    this._value = val;
                    while (this._onSuccess.length > 0) {//如果队列有方法,则进行调用
                        let temp = this._onSuccess.shift();
                        temp(val);
                    }
                }
            }
            let reject = (val)=>{//传递给外部去主动调用的失败函数
                if (this.status === 'pending') {
                    this.status = 'resolve';
                    this._value = val;
                    while (this._onFail.length > 0) {//如果队列有方法,则进行调用
                        let temp = this._onFail.shift();
                        temp(val);
                    }
                }
            }
            callback(resolve, reject)
        }
        then(onFulfilled, onRejected) {
            switch (this.status) {//状态判断
            case 'resolve'://成功,调用成功回调函数,并传值
                onFulfilled(this._value);
                break;
            case 'reject'://失败,调用失败回调函数,并传值
                onRejected(this._value);
                break;
            case 'pending'://等待状态,则先将函数放入队列中,等待外部调用触发resolve/reject,再进行调用
                this._onSuccess.push(onFulfilled);
                this._onFail.push(onRejected);
                break;
            }
        }
    }
    //测试case
    const pro = new SimplePromise(function(res, rej) {
        setTimeout(function() {
            let random = Math.random() * 10
            if (random > 5) {
                res("success")
            } else {
                rej("fail")
            }
        }, 2000)

    }
    )

    pro.then(function(data) {
        console.log(data)
    }, function(err) {
        console.log(err)
    })
Promise.prototype.all = (promises = [])=>{
    return new Promise((resolve,reject)=>{
        let resultArr = [];
        let count = 0;
        promises.forEach((p,i)=>{
           Promise.resolve(p).then((value)=>{
                count++;
                resultArr[i] = value;
               if(count === promises.length){
                    resolve(resultArr)
               }
           },reject)
        })
    })
}

Promise.prototype.race = (promises = []) => {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises)) {
            throw new TypeError(`argument must be a array`);
        }
        for (const p of promises) {
            // 有一个成功就返回成功状态的promise
            // 有一个失败就返回失败状态的promise
            p.then(resolve, reject);
        }
    });
}

let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
Promise.all([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
});
Promise.race([p3, p1, p2]).then(res => {
    console.log(res) // 1
});

实现一个异步队列,可以设置并发请求数量,应用场景例如:分片上传可以控制三个任务

class PromiseQueue{
    constructor(limit=1){
        this._list = [];
        this._current= 0;
        this._limit = limit;
    }
    add(promiseFn){
        this._list.push(promiseFn);
        this.loadNext();
    }
    loadNext(){
        if(this._list.length === 0 || this._limit === this._current) return;
        this._current++;
        const fn = this._list.shift();
        const promise = fn();
        promise.then(this.onLoaded.bind(this)).catch(this.onLoaded.bind(this));
    }
    onLoaded(){
        this._current--;
        this.loadNext();
    }
}

const q = new PromiseQueue(2);
[1, 2, 3, 4, 5].forEach(v => {
    q.add(() => new Promise((resolve) => {
       setTimeout(() => {
           console.log(v);
           resolve();
       }, 1000);
    }));
});
SunXinFei commented 3 years ago

image

防抖&截流

function debounce(func, delay){
        let id;
        return function (...args){
           clearTimeout(id);
           id = setTimeOut(()=>{
               func.apply(this,args)
           },delay)

        }
    }

防抖(debounce) :在事件触发后的n秒之后,再去执行真正需要执行的函数,如果在这n秒之内事件又被触发,则重新开始计时。常见的操作就是搜索,中间不断的输入,我们都可以忽略,只获取最后停止输入的时候,才去请求后端。

//截流
/**
 * 连续的方法 延迟,如持续输入数据,搜索
 * @param {Function} fn 需要延迟运行的的回调函数
 **/
let previous = 0
export const throttle = (fn,delay) => {
    return function() {
        let now = +new Date();
        if (now - previous > delay) {
          fn.apply(this, arguments);
          previous = now;
        }
    }
}
//调用地方
    /**
     * 处理滚动视图的滚动事件
     */
    scrollFn(e) {
      throttle(this.aaa, 1000)(666);
    },
   aaa(num){
      console.log(this.test,num);
    },

节流(throttling):规定好一个单位时间,触发函数一次。如果在这个单位时间内触发多次函数的话,只有一次是可被执行的。想执行多次的话,只能等到下一个周期里。常见的操作比如滚动事件,每隔n毫秒,我们去请求,或者拖拽,每隔n毫秒改变dom的位置。还比如resize窗口。

SunXinFei commented 3 years ago

image

SunXinFei commented 3 years ago

异常捕获

SunXinFei commented 3 years ago

网络安全

XSS跨站脚本攻击

方式有很多种,大部分都是进行脚本注入,举个例子:在url里面做操作,将普通访问的url修改为url+script脚本,scrript脚本里面可以做很多功能比如:获取cookie并发送到黑客的服务器(用cookie的httponly解),去提交一个数据...等等。 前端的框架都有成熟的xss跨站脚本防御,一般是用户侧提交的东西,都会进行编码,来防止注入。

CSRF跨站请求伪造

这个一般就是首先在A这个安全网站登陆,用户打开了B这个危险网站,B网站里面加了一个img标签,src是A网站的一个get请求,这时候浏览器会将A网站的请求发出去,并且自动携带了A的cookie,A网站后台没有任何保护的情况下就以为是用户在操作,从而被攻击。 使用token解决就是利用了同源策略里面B网站是拿不到A网站任何dom元素或者js脚本执行结果的,将A网站上html加入后端生成的token,发送请求的时候携带上(header中或者链接中都可以),后端拿着token去校验有效性。这样危险网站B就因为不容易伪造token,而伪装失败。 或者设置: Set-Cookie: CookieName=CookieValue; SameSite=Strict; Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。 Set-Cookie: CookieName=CookieValue; SameSite=Lax; Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。 Set-Cookie: widget_session=abc123; SameSite=None; Secure Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。 http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie

点击劫持

最常见的是恶意网站使用