js知识总结—理论知识篇(二)

1.js事件循环机制

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

js知识总结—理论知识篇(二)

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )、requestAnimationFrame

宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval

微任务主要包含PromiseMutaionObserver(IOS)、process.nextTick(Node.js )

微任务的优先级: process.nextTick > Promise > MutationObserver

console.log('script start');

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

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

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

//-------------------------------------------

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

2.setTimeout(fn,100)中的100毫秒时如何权衡的

当我们执行到setTimeout定时器代码时候,会把代码放到一个定时触发器线程,然后开始计时。计时结束后,会将定时的回调函数插入任务队列。必须等到主线程中的代码及先进入队列的任务执行完,主线程才会去执行这个回调。所以有可能要等很久,所以没办法保障回调函数一定会来100毫秒后执行。

这里注意,并不是等主线程执行完毕才开始计时,而是执行到定时器代码块时候就开始计时了。影响回调函数执行时间的是主线程的执行时间。

定时触发器线程

浏览器定时计数器并不是由JS引擎计数的,,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确。因此通过单独线程来计时并触发定时是更为合理的方案。

3.js基本数据类型和引用数据类型的区别

  1. js基本数据类型有:Number、String 、Boolean、Null和Undefined;引用类型有:Object 、Array 、Function。
  2. 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,当这个变量重新赋值之后看起来变量的值是变了,但这里实际只是变量名指向变量的一个指针变了(改变的是指针的指向)。该变量是不会变的。引用类型时可以被改变的。一旦重新赋值,其值则被改变。
  3. 基本数据类型不可以添加属性和方法,但是引用类型可以。
  4. 基本数据类型的赋值都是简单赋值,如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新的值。然后把该值复制到为新变量分配的内存位置上。而引用类型的赋值是对象的引用。
  5. 基本数据类型的比较是值的比较,引用类型的比较是引用的比较。引用数据了类型比较的是内存地址是否相同。
  6. 基本数据类型是存放在栈区的,引用数据类型会同时保存在栈区和堆区。栈区保存的是一个地址。

4.一个单机网页游戏老是卡顿或崩溃,原因可能是什么

  1. 内存溢出问题:及时清理内存,将无用对象回收(垃圾回收机制)
  2. 资源文件过大:选择较小的图片或压缩图片
  3. 资源加载过慢:预加载资源,在游戏初始化之前就加载资源
  4. 动画绘制频率问题:每一帧的绘制间隔时间与显示器刷新频率保持一致。
  5. 单机游戏与网络无关。如果是网络游戏,ping值过高,请确认自己网络是否延迟过高。

5.函数式编程思想

定义:函数式编程(FP)是一种编程范式(其针对与面向对象编程范式OOP,函数式与面向对象冲突)。函数式编程是把运算过程尽量写成一系列嵌套的函数调用。 这里的函数式指的是数学函数,而非编程语言中的函数。

纯函数:函数式编程允许我们编写的函数是纯函数。纯函数的特点:

  1. 如果给定相同的参数,则返回相同的结果(也称为确定性)。可以理解为返回结果不因除参数外的任何变量的改变而改变。
  2. 它不会引起任何副作用。比如修改全局对象或修改通过引用传递的参数。

函数式编程的特点:

  1. 数据和行为分离(OOP是把数据和行为打包)
  2. 不可修改性(immutability。OOP以及面向过程编程很多时候都会对内存状态进行修改,比如给变量或成员重新赋值、往一个集合里添加/删除数据等,FP不允许这么干)
  3. 一等公民是函数(函数可以赋值给变量,可以当参数传递,函数的返回值可以是函数。OOP里的一等公民只有对象)
  4. 闭包

函数式编程的优势:

  1. 并行:纯函数无需任何修改即可并行执行。
  2. 无副作用:不会修改全局变量和引用变量,不会干扰其他程序。
  3. 更容易测试:函数式程序的 bug 不依赖于与任何与其无关的代码,你遇到的问题就总是可以再现。我们可以使用不同的上下文对纯函数进行单元测试。
  4. 引用透明性:如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
  5. 不可变性:只要参数不变,返回值永远不变
  6. 高内聚低耦合:函数是一阶公民。函数可以作为参数或返回值。我们可以组合不同的函数来创建具有新行为的新函数。

6.JS函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

Currying有哪些好处

① 提高复用率

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

② 提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

// 提前确认调用哪个方法,不用每次调用都判断
var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

③ 延迟运行

Function.prototype.bind = function (context) {
    var _this = this,args = Array.from(arguments).slice(1)
    return function() {
        return _this.apply(context, args)
    }
}

7.立即执行函数的作用

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 立即执行函数执行完后就立刻被销毁,避免常驻内存。(所以命名的立即执行函数外部无法访问,实际情况是已被销毁)
(function foo() {
    console.log(123);
})();
//已被销毁,所以访问不到。所以一般用匿名函数
foo();  // ReferenceError: foo is not defined

// 要访问自执行函数的执行结果
var num = (function (a, b) {
    return a + b;
})(1, 2);

//其实这种写法不用括号把函数包起来也是可以的
var num = function (a, b) {
    return a + b;
}(1, 2);
//在执行赋值操作的时候,上面的代码已经变成了一个表达式,而 JavaScript 解释器认为 函数表达式 是可以直接被执行的。

// 如果要再次访问自执行函数,需要将其作为返回值
// 这时就不能用匿名函数了
var num = (function aaa(a, b) {
    console.log(a+b)
    return aaa
})(1, 2);
num(3,4)
num(3,4)(5,6)

// 利用arguments.callee方法返回当前函数用于访问自执行函数
// 这样免得给匿名函数命名
// 但arguments相关的方法在严格模式下会报错
var num = (function(a, b) {
    console.log(a+b)
    return arguments.callee
})(1, 2);

使用场景:

  1. 立即执行函数执行完后就立刻被销毁,所以如果函数只被执行一次的时候就可以将其换成立即执行函数。
  2. 创建独立作用域,避免变量干扰。
  3. 用于初始化方法(数据初始化)。可避免内部变量污染全局。
  4. 可以用来封装一些插件、轮子、方法。

ES6中,块级作用域已经可以取代立即执行函数。可以将需要立即执行的代码放到花括号{}中。

8.定时器的最小间隔(延迟)时间

HTML5标准规定

  • setTimeout的最短时间间隔是4毫秒;
  • setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒
加载中...
加载中...