JS 相关

277 阅读44分钟

异步加载js的方法

defer

IE4.0就出现。defer属声明脚本中将不会有document.write和dom修改。浏览器会并行下载其他有defer属性的script。而不会阻塞页面后续处理。注:所有的defer脚本必须保证按顺序执行的。

<script type="text/javascript" defer></script>

async

HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。作用同defer,但是不能保证脚本按顺序执行。他们将在onload事件之前完成。

<script type="text/javascript" async></script>

或者,创建script标签,插入到DOM中,且加上async属性

(function(){
    var scriptEle = document.createElement("script");
    scriptEle.type = "text/javasctipt";
    scriptEle.async = true;
    scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
    var x = document.getElementsByTagName("head")[0];
    x.insertBefore(scriptEle, x.firstChild);
 })();

但是这种加载方式执行完之前会阻止onload事件的触发,而现在很多页面的代码都在onload时还执行额外的渲染工作,所以还是会阻塞部分页面的初始化处理。

onload时的异步加载

由于以上两种方法都是在onload事件之前完成,因此,会阻塞onload加载,而现在很多页面的代码都在onload时还执行额外的渲染工作,所以还是会阻塞部分页面的初始化处理。

(function () {
    if (window.attachEvent) {
        window.attachEvent("load", asyncLoad);
    } else {
        window.addEventListener("load", asyncLoad);
    }
    function asyncLoad() {
        var ga = document.createElement('script');
        ga.async = true;
        ga.src = "http:www.baidu.com";
        var s = document.getElementsByTagName('head')[0];
        s.appendChild(ga);
    }
})();

准确判断JS数据类型

Object.prototype.toString()

toString方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。

var o1 = new Object();
o1.toString() // "[object Object]"

var o2 = {a:1};
o2.toString() // "[object Object]"

上面代码表示,对于一个对象调用toString方法,会返回字符串[object Object],该字符串说明对象的类型。

字符串[object Object]本身没有太大的用处,但是通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式。

var obj = new Object();

obj.toString = function () {
  return 'hello';
};

obj + ' ' + 'world' // "hello world"

上面代码表示,当对象用于字符串加法时,会自动调用toString方法。由于自定义了toString方法,所以返回字符串hello world

数组、字符串、函数、Date 对象都分别部署了自定义的toString方法,覆盖了Object.prototype.toString方法。

[1, 2, 3].toString() // "1,2,3"

'123'.toString() // "123"

(function () {
  return 123;
}).toString()
// "function () {
//   return 123;
// }"

(new Date()).toString()
// "Tue May 10 2016 09:11:31 GMT+0800 (CST)"

上面代码中,数组、字符串、函数、Date 对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖原始方法。

toString() 的应用:判断数据类型

Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型。

var obj = {};
obj.toString() // "[object Object]"

上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。

由于实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。

Object.prototype.toString.call(value)

上面代码表示对value这个值调用Object.prototype.toString方法。

不同数据类型的Object.prototype.toString方法返回值如下。

  • 数值:返回[object Number]
  • 字符串:返回[object String]
  • 布尔值:返回[object Boolean]
  • undefined:返回[object Undefined]
  • null:返回[object Null]
  • 数组:返回[object Array]
  • arguments 对象:返回[object Arguments]
  • 函数:返回[object Function]
  • Error 对象:返回[object Error]
  • Date 对象:返回[object Date]
  • RegExp 对象:返回[object RegExp]
  • 其他对象:返回[object Object]

这就是说,Object.prototype.toString可以看出一个值到底是什么类型。

Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"

利用这个特性,可以写出一个比typeof运算符更准确的类型判断函数。

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法。

const type = {}
function getType(o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

['Null', 'Undefined', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp' ].forEach(function (t) {
  type['is' + t] = function (o) {
    return getType(o) === t.toLowerCase();
  };
});

type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true

其他判断数据类型的方法

constructor

判断构造函数是否为 Object

obj.constructor === Object

instanceof

判断是否为某个构造函数的实例对象

需要注意的是由于数组也是对象,因此用 arr instanceof Object 也为true。

obj instanceof Object

因此,可以先判断是否为对象,再判断是否为数组

obj instanceof Object&&!Array.isArray(obj)

4. typeof

根据typeof判断对象也不太准确

typeof obj === Object
typeof undefined	       // 'undefined'
typeof null	               // 'object'
typeof true	               // 'boolean'
typeof 123	               // 'number'
typeof "abc"	           // 'string'
typeof function() {}	   // 'function'
typeof {}	               // 'object'
typeof []	               // 'object'

同样的可以排除法

obj&&typeof obj === Object&&!Array.isArray(obj)

Promise then 和catch 相关

  • reject或者手动抛出错误的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch
 var p1=new Promise((resolve,rej) => {
    //throw new Error('手动返回错误')
    rej('失败了')
 })

 p1.then(data =>{
    console.log('data::',data);
 },err=> {
    console.log('err::',err)
 }).catch(
    res => {
    console.log('catch data::', res)
 })

结果:

err:: 失败了
  • then中没有第二个回调的情况,则进入catch
 var p1=new Promise((resolve,rej) => {
    //throw new Error('手动返回错误')
    rej('失败了')
 })

 p1.then(data =>{
    console.log('data::',data);
 }).catch(
    res => {
    console.log('catch data::', res)
 })

结果:

catch data:: 失败了
  • 如果没有then,reject或者手动抛出错误的也可以直接进入catch
 var p1=new Promise((resolve,rej) => {
    console.log('没有 resolve')
    //throw new Error('手动返回错误')
    rej('失败了')
 })

 p1.catch(
    res => {
    console.log('catch data::', res)
 })

结果:

没有resolve
catch data:: 失败了
  • resolve的东西,一定会进入then的第一个回调,肯定不会进入catch
 var p1=new Promise((resolve,rej) => {
    resolve('成功了')
 })

 p1.then(data =>{
    console.log('data::',data);
 }).catch(
    res => {
    console.log('catch data::', res)
 })

结果:

data:: 成功了
  • throw new Error 的情况和rej一样,但是他俩只会有一个发生
  • 另外,网络异常(比如断网),会直接进入catch而不会进入then的第二个回调

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面所有then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

事件循环(Event loop)

单线程模型

单线程模型指的是:JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。

注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。

JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。这种模式的优缺点:

  • 好处是:实现起来比较简单,执行环境相对单纯;

  • 坏处是:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。

如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。

单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

浏览器多线程

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

JS是单线程语言,浏览器只分配给JS一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。

浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程浏览器定时触发器浏览器事件触发线程,这些任务是异步的。

同步和异步

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

  • 同步任务:那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

  • 异步任务:那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。

任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

主线程:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

任务队列:当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查(观察者模式),一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。这里还有一个执行栈的概念。

执行栈:当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,遵循先进先出原则。要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。

image-20210622154735615

关于执行栈,主线程,任务队列,其实可以简单的理解为:任务队列是不同的学生,主线程是裁判,执行栈是跑道,主线程判断任务队列里的哪个任务在执行栈执行

如下:

let a = () => {
  setTimeout(() => {
    console.log('任务队列函数1')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('a的for循环')
  }
  console.log('a事件执行完')
}
let b = () => {
  setTimeout(() => {
    console.log('任务队列函数2')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('b的for循环')
  }
  console.log('b事件执行完')
}
let c = () => {
  setTimeout(() => {
    console.log('任务队列函数3')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('c的for循环')
  }
  console.log('c事件执行完')
}
a();
b();
c();
// 当a、b、c函数都执行完成之后,三个setTimeout才会依次执行

宏任务与微任务

异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

宏任务(macrotask):当前调用栈中执行的代码称为宏任务。(主代码快,定时器等等)。

script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)

微任务(microtask):当前(此次事件循环中)宏任务执行完(script整体代码也是宏任务),在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。(promise.thenproness.nextTick等等)。

Promise.thenMutaionObserverprocess.nextTick(Node.js环境)

宏任务中的事件放在callback queue中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。

注意:先执行微任务,再执行宏任务。

总结事件循环:

  • 所有同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个"任务队列"(task queue)。当遇到异步任务,首先挂起,只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,先微观任务进入执行栈并开始执行,再宏观任务进入执行栈并开始执行。

看完了事件循环的机制,我们再回头研究一下定时器

定时器运行机制

setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

setTimeout(someTask, 100);
veryLongTask();

上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。

再看一个setInterval的例子。

setInterval(function () {
  console.log(2);
}, 1000);

sleep(3000);

function sleep(ms) {
  var start = Date.now();
  while ((Date.now() - start) < ms) {
  }
}

上面代码中,setInterval要求每隔1000毫秒,就输出一个2。但是,紧接着的sleep语句需要3000毫秒才能完成,那么setInterval就必须推迟到3000毫秒之后才开始生效。注意,生效后setInterval不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2。

所以,其实定时器是不准确的,可以说执行时间一定大于设置的时间。

面试题实践

示例一:

console.log('开始111');
setTimeout(function() {
  console.log('setTimeout111');
});
Promise.resolve().then(function() {
  console.log('promise111');
}).then(function() {
  console.log('promise222');
});
console.log('开始222');

我们按照步骤来分析下:

  1. 遇到同步任务,直接先打印 “开始111”。
  2. 遇到异步 setTimeout ,先放到任务队列中等待执行。
  3. 遇到了 Promise 的 then,放到等待队列中。
  4. 遇到了 Promise 的 then,放到等待队列中。
  5. 遇到同步任务,直接打印 “开始222”。
  6. 同步执行完,返回任务队列中的代码,从上往下执行,发现有宏观任务 setTimeout 和微观任务 Promise 的 then,那么先执行微观任务,再执行宏观任务。

所以打印的顺序为: 开始111 、开始222 、 promise111 、 promise222 、 setTimeout111 。

注意:对于promise而言,内部代码都是同步,只有then()才是异步,new Promise() 是同步

示例二:

console.log('开始111');
setTimeout(function () {
  console.log('timeout111');
});
new Promise(resolve => {
  console.log('promise111');
  resolve();
  setTimeout(() => console.log('timeout222'));
}).then(function () {
  console.log('promise222')
})
console.log('开始222');

分析一下:

  1. 遇到同步代码,先打印 “开始111” 。
  2. 遇到setTimeout异步,放入队列,等待执行 。
  3. 中途遇到Promise函数,函数直接执行,打印 “promise111”。
  4. 遇到setTimeout ,属于异步,放入队列,等待执行。
  5. 遇到Promise的then等待成功返回,异步,放入队列。
  6. 遇到同步,打印 “开始222”。
  7. 执行完,返回,将异步队列中的代码,按顺序执行。有一个微观任务,then后的,所以打印 “promise222”,再执行两个宏观任务 “timeout111” “timeout222”。

所以,打印的顺序为:开始111 、 promise111 、 开始222 、 promise222 、 timeout111 、 timeout222 .

requestAnimationFrame详解

setTimeoutsetInterval不同,requestAnimationFrame不需要设置时间间隔。这有什么好处呢?为什么requestAnimationFrame被称为神器呢?本文将详细介绍HTML5新增的定时器requestAnimationFrame

引入

  计时器一直是javascript动画的核心技术。而编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅;另一方面,循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化

  大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms

  而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行

  requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果

特点

  【1】requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

  【2】在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量

  【3】requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销

使用

  requestAnimationFrame的用法与settimeout很相似,只是不需要设置时间间隔而已。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行

requestID = requestAnimationFrame(callback); 
//控制台输出1和0
var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
console.log(timer);//1

  cancelAnimationFrame方法用于取消定时器

//控制台什么都不输出
var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
cancelAnimationFrame(timer);

  也可以直接使用返回值进行取消

var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
cancelAnimationFrame(1);

兼容

  IE9-浏览器不支持该方法,可以使用setTimeout来兼容

【简单兼容】

if (!window.requestAnimationFrame) {
    requestAnimationFrame = function(fn) {
        setTimeout(fn, 17);
    };    
}

【严格兼容】

if(!window.requestAnimationFrame){
    var lastTime = 0;
    window.requestAnimationFrame = function(callback){
        var currTime = new Date().getTime();
        var timeToCall = Math.max(0,16.7-(currTime - lastTime));
        var id  = window.setTimeout(function(){
            callback(currTime + timeToCall);
        },timeToCall);
        lastTime = currTime + timeToCall;
        return id;
    }
}
if (!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function(id) {
        clearTimeout(id);
    };
}

应用

  现在分别使用setInterval、setTimeout和requestAnimationFrame这三个方法制作一个简单的进制度效果

【1】setInterval

<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
    clearInterval(timer);
    myDiv.style.width = '0';
    timer = setInterval(function(){
        if(parseInt(myDiv.style.width) < 500){
            myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
            myDiv.innerHTML =     parseInt(myDiv.style.width)/5 + '%';    
        }else{
            clearInterval(timer);
        }        
    },16);
}
</script>

复制代码

【2】setTimeout

<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
    clearTimeout(timer);
    myDiv.style.width = '0';
    timer = setTimeout(function fn(){
        if(parseInt(myDiv.style.width) < 500){
            myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
            myDiv.innerHTML =     parseInt(myDiv.style.width)/5 + '%';
            timer = setTimeout(fn,16);
        }else{
            clearTimeout(timer);
        }    
    },16);
}
</script>

【3】requestAnimationFrame

<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
    myDiv.style.width = '0';
    cancelAnimationFrame(timer);
    timer = requestAnimationFrame(function fn(){
        if(parseInt(myDiv.style.width) < 500){
            myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
            myDiv.innerHTML =     parseInt(myDiv.style.width)/5 + '%';
            timer = requestAnimationFrame(fn);
        }else{
            cancelAnimationFrame(timer);
        }    
    });
}
</script>

this指向问题

前言

this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。

简单说,this就是属性或方法“当前”所在的对象。

this.property

上面代码中,this就代表property属性当前所在的对象。

下面是一个实际的例子。

var person = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

person.describe()
// "姓名:张三"

上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向personthis.name就是person.name

由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的。

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

B.describe = A.describe;
B.describe() // "姓名:李四"

上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name

只要函数被赋给另一个变量,this的指向就会变。

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var name = '李四';
var f = A.describe;
f() // "姓名:李四"

上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象,在浏览器中就是window),因此name为全局name的值。

实质

JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系。

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

这样的结构是很清晰的,问题在于属性的值可能是一个函数

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

f() // 单独执行

obj.f() // obj 环境执行

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境

var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的xthis 就是指代当前运行在什么环境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x

使用场合

this主要有以下几个使用场合。

全局环境下

  • 在浏览器全局环境下,this 始终指向全局对象(window), 无论是否严格模式;
this === window // true
  • 普通函数,非严格模式下,指向window严格模式下,指向 undefined
function f() {
    console.log(this === window);
}
f() // true

function f() {
    'use strict';
    console.log(this === undefined);
}
f() // true

构造函数中

构造函数中的this,指的是实例对象。

var Obj = function (p) {
  this.p = p;
};

上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性。

var o = new Obj('Hello World!');
o.p // "Hello World!"

对象的方法中

如果对象的方法里面包含thisthis的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。

但是,这条规则很不容易把握。请看下面的代码。

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj

上面代码中,obj.foo方法执行时,它内部的this指向obj

但是,下面这几种用法,都会改变this的指向。

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj

可以这样理解,JavaScript 引擎内部,objobj.foo储存在两个内存地址,称为地址一地址二

obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一this指向obj

但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码:

// 情况一
(obj.foo = function () {
  console.log(this);
})()
// 等同于
(function () {
  console.log(this);
})()

// 情况二
(false || function () {
  console.log(this);
})()

// 情况三
(1, function () {
  console.log(this);
})()

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined

上面代码中,a.b.m方法在a对象的第二层,该方法内部的this不是指向a,而是指向a.b,因为实际执行的是下面的代码。

var b = {
  m: function() {
   console.log(this.p);
  }
};

var a = {
  p: 'Hello',
  b: b
};

(a.b).m() // 等同于 b.m()

数组方法中的this

数组的mapforeach方法,允许提供一个函数作为参数。这个函数内部不应该使用this

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// undefined a1
// undefined a2

上面代码中,foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。

解决这个问题的一种方法,就是前面提到的,使用中间变量固定this

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2

另一种方法是将this当作foreach方法的第二个参数,固定它的运行环境。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2

或者使用es6的箭头函数。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach((item)=>{
        console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// hello a1
// hello a2

原型链中 this

  • 原型链中的方法的this仍然指向调用它的对象
var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5
  • 以上代码,可以看出, 在p中没有属性f,当执行p.f()时,会查找p的原型链,找到 f 函数并执行,但这与函数内部this指向对象 p 没有任何关系,只需记住谁调用指向谁。

DOM事件处理函数

  • 事件处理函数内部的 this 指向触发这个事件的对象
var oBox = document.getElementById('box');
oBox.onclick = function () {
    console.log(this)   //oBox
}

setTimeout & setInterval

对于延时函数内部的回调函数的this指向全局对象window(当然我们可以通过bind方法改变其内部函数的this指向)

//默认情况下代码
function Person() {  
    this.age = 0;  
    setTimeout(function() {
        console.log(this);
    }, 3000);
}

var p = new Person();//3秒后返回 window 对象
==============================================
//通过bind绑定
function Person() {  
    this.age = 0;  
    setTimeout((function() {
        console.log(this);
    }).bind(this), 3000);
}

var p = new Person();//3秒后返回构造函数新生成的对象 Person{...}

箭头函数中的 this

由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值,

  • 所以 call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的 this 毫无影响。
  • 考虑到 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。(可以忽略是否在严格模式下的影响)
function Person() {  
    setInterval(() => {
        console.log(this)	//Person
    }, 3000);
}

var p = new Person();

以上 this 指向 Person

let a = ()=> {
  console.log(this)
}
a()

以上代码 this 指向 window

this的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,避免出现意想不到的情况。JavaScript 提供了callapplybind这三个方法,来切换/固定this的指向。以下是绑定 this 的方法。

Function.prototype.call()

使用

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f

call方法的参数,应该是一个对象。如果参数为空、nullundefined,则默认传入全局对象。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。如果使用call方法将this关键字指向obj对象,返回结果为456。可以看到,如果call方法没有参数,或者参数为nullundefined,则等同于指向全局对象。

注意:如果是严格模式或者vue(vue默认严格模式)中,传入null则指向null,传入window则指向window

如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。

var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

上面代码中,call的参数为5,不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this

参数

call方法还可以接受多个参数。

func.call(thisValue, arg1, arg2, ...)

call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为12,因此函数add运行后得到3

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代码中,通过obj对象继承ObjecthasOwnProperty方法判断obj对象自身是没有 toString这个方法的,hasOwnPropertyobj对象继承的方法,如果这个方法一旦被覆盖(这里的覆盖并不是覆盖了Object原型上的方法,而是在obj创建了与原型一样的方法,执行的时候优先调用自身的方法),就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。

手动实现

实现思路:

  • 改变this指向:可以将目标函数作为这个对象的属性
  • 利用arguments类数组对象实现参数不定长
  • 不能增加对象的属性,所以在结尾需要delete

以下是手动实现方式:

/**
*@desc 描述
*@param object [Object] 需要指向的对象
*/
Function.prototype.myCall = function (object) {
    let obj = object || window;	// 如果没有传this参数,this将指向window
    var fn = Symbol();       //  Symbol属性来确定fn唯一
    obj[fn] = this;				// 这里的this指向调用myCall的函数(代指a函数),同时将a函数的引用赋值给obj的fn属性。此时,当a函数调用的时候就是指向obj的了
    let arg = [...arguments].slice(1);	// 获取第二个及后面的所有参数(arg是一个数组)
    let result = obj[fn](...arg); // a函数的引用调用,指向obj,也就是传入的对象
    delete obj[fn] // 不能增加obj的属性,所以要删除
    return result // 如果a函数有返回值这里就有返回值,如果a函数没有返回值,同样的这里也没有
}

以上代码:

  • 当myCall没有传参的时候,做兼容,指向window。
  • myCall函数中this指向调用者,也就是执行myCall的函数,这里称之为a函数。
  • 将a函数的引用赋值给obj.fn,等同于a函数执行的时候,内部的this指向obj。这里就实现了this的绑定。
  • 将a参数使用arguments通过slice取出,当然,a函数的参数是从第二位开始,因此是slice(1)
  • 执行obj.fn等同于执行a函数,返回结果也等同于a函数的返回结果,如果a函数有返回值,则result有值,反之则没有。

以下是执行结果:

function a(c) {
    console.log('aaa', this.a + this.b + c);
}
const obj = {
    a: 1,
    b: 2
}
a.myCall(obj, 1) // 打印 4

myCall改变了a函数内的指向,指向objx相当于obja的实例,即obj调用a,a.myCall(obj, 1)等价于obj.a(1)因此打印 1+2+1=4

Function.prototype.apply()

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一个参数也是this所要指向的那个对象,如果设为nullundefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

上面代码中,f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

利用这一点,可以做一些有趣的应用。

将数组的空元素变为undefined

通过apply方法,利用Array构造函数将数组的空元素变成undefined

Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ]

空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined。因此,遍历内部元素的时候,会得到不同的结果。

当然,es6可以使用扩展运算符实现。

const arr = ['a',,'b'];
[...arr] // 'a',undefined,'b'

转换类似数组的对象

另外,利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

上面代码的apply方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length属性,以及相对应的数字键。

手动实现

其实call和applay之间的差别就是后面的传参

/**
*@desc 描述
*@param object [Object] 需要指向的对象
*/
Function.prototype.myCall = function (object) {
    let obj = object || window;	// 如果没有传this参数,this将指向window
    var fn = Symbol();       //  Symbol属性来确定fn唯一
    obj[fn] = this;				// 这里的this指向调用myCall的函数(代指a函数),同时将a函数的引用
    let arg = [...arguments].slice(1);	// 获取第二个及后面的所有参数(arg是一个数组)
    let result = obj[fn](arg); // a函数的引用调用,指向obj,也就是传入的对象,这里直接传一个数组
    delete obj[fn] // 不能增加obj的属性,所以要删除
    return result // 如果a函数有返回值这里就有返回值,如果a函数没有返回值,同样的这里也没有
}

和call的区别就在于,以下代码传一个数组

let result = obj.fn(arg);

Function.prototype.bind()

bind()方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

上面代码中,我们将d.getTime()方法赋给变量print,然后调用print()就报错了。这是因为getTime()方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。

bind()方法可以解决这个问题。

var print = d.getTime.bind(d);
print() // 1481869925657

上面代码中,bind()方法将getTime()方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。

bind方法的参数就是所要绑定this的对象,下面是一个更清晰的例子。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1

上面代码中,counter.inc()方法被赋值给变量func。这时必须用bind()方法将inc()内部的this,绑定到counter,否则就会出错。

bind()还可以接受更多的参数,将这些参数绑定原函数的参数。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5); // 将5传给x
newAdd(5) // 20 将5传给y

上面代码中,bind()方法除了绑定this对象,还将add()函数的第一个参数x绑定成5,然后返回一个新函数newAdd(),这个函数只要再接受一个参数y就能运行了。

注意:如果bind()方法的第一个参数是nullundefined,等于将this绑定到全局对象,函数运行时this指向顶层对象(浏览器为window)。

bind()方法有一些使用注意点。

每一次返回一个新函数

bind()方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。

element.addEventListener('click', o.m.bind(o));

上面代码中,click事件绑定bind()方法生成的一个匿名函数。这样会导致无法取消绑定,所以下面的代码是无效的。

element.removeEventListener('click', o.m.bind(o));

正确的方法是写成下面这样:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);

结合回调函数使用

回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含this的方法直接当作回调函数。解决方法就是使用bind()方法,将counter.inc()绑定counter

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};

function callIt(callback) {
  callback();
}

callIt(counter.inc.bind(counter));
counter.count // 1

上面代码中,callIt()方法会调用回调函数。这时如果直接把counter.inc传入,调用时counter.inc()内部的this就会指向全局对象。使用bind()方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter

还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的this指向,很可能也会出错。

var obj = {
  name: '张三',
  times: [1, 2, 3],
  print: function () {
    this.times.forEach(function (n) {
      console.log(this.name);
    });
  }
};

obj.print()
// 没有任何输出

上面代码中,obj.print内部this.timesthis是指向obj的,这个没有问题。但是,forEach()方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值

解决这个问题,也是通过bind()方法绑定this

obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this.name);
  }.bind(this));
};

obj.print()
// 张三
// 张三
// 张三

结合call()方法使用

利用bind()方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice()方法为例。

[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定的开始位置和结束位置,切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice()方法,因此可以用call方法表达这个过程,得到同样的结果。

call()方法实质上是调用Function.prototype.call()方法,因此上面的表达式可以用bind()方法改写。

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

上面代码的含义就是:

call 方法是Function原型上的方法,而slice也是方法。因此,通过bind改变call的指向,指向sliceslice就相当于Function的实例,callslice实例下的一个方法,也就是将slice变成call方法所在的对象。当call执行的时候,其实就等同于slice调用了call方法 Array.prototype.slice.call

类似的写法还可以用于其他数组方法。

var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。

function f() {
  console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

上面代码的含义就是:

  • 通过bind改变call的指向,指向bind。也就是bind成为了call的实例,相当于bind调用call。因此,返回Function.prototype.bind.call函数。

  • bind(f,o) 的含义是:调用Function.prototype.bind.call函数,第一个参数是改变bind指向,指向f第二个参数bind的参数。也就是f调用了bind函数,结果返回一个指向of函数。

  • 然后再加括号执行bind(f,o)(),就等于执行了指向of函数。最后打印结果的this指向o,因此打印出123

手动实现

同call和applay不同的是,bind返回一个函数

/**
      *@desc 描述
      *@param object [Object] 需要指向的对象
      */
Function.prototype.myBind = function (object) {
    let obj = object || window;	// 如果没有传this参数,this将指向window
    var fn = Symbol();       //  Symbol属性来确定fn唯一
    obj[fn] = this;				// 这里的this指向调用myCall的函数(代指a函数),同时将a函数的引用
    let arg = [...arguments].slice(1);	// 获取第二个及后面的所有参数(arg是一个数组)
    /* 返回一个函数,函数内部执行调用的a函数,并传入参数 */
    const fBind = function () {
        /* 如果当前函数执行中的this是fBound的实例,说明是fBind被new了,那么当前 this就是函数的实例,否则是obj */
        if (this instanceof fBind) {
            object[fn].applay(this, arg)
        } else {
            object[fn](...arg)
        }
        delete object[fn]
    }
    return fBind
}
function a(c) {
    console.log('aaa', this.a + this.b + c);
}
const obj = {
    a: 1,
    b: 2
}
a.myBind(obj, 1)(); // 打印 4

算法

实现斐波拉契数列

  • 数学上是以递归的方法来定义,从0开始,即F(0)=0,F(1)=1。第三位的值等于前两位相加。即F(2)=F(0)+F(1)=0+1,以此类推。
F(0) = 0;
F(1) = 1;
F(n) = F(n - 1) + F(n - 2);
  • 公式版:递归
function fib(n) {
    if (n < 0) throw new Error('输入的数字不能小于0');
    if (n < 2) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

正常递归版本是一个既简单又直接的逻辑,但是这个版本有个问题就是存在大量重复计算。如:当 n 为 5 的时候要计算fib(4) + fib(3)当 n 为 4 的要计算fib(3) + fib(2) ,这时fib(3)就是重复计算了。运行 fib(50) 等半天才会出结果。

  • 去除重复计算的递归版本
    function fib(n) {
        if (n < 0) throw new Error('输入的数字不能小于0');
        if (n < 2) return n;
        function _fib(n, a, b) {
            if (n === 0) return a;
            return _fib(n - 1, b, a + b);
        }
        return _fib(n, 0, 1);
    }

把前两位数字做成参数巧妙的避免了重复计算,性能也有明显的提升。n 做递减运算,前两位数字做递增(斐波那契数列的递增), 这段代码一个减,一个增。

  • 数组方法
function fib(n) {
    if (n < 0) throw new Error('输入的数字不能小于0');
    if (n < 2) {
        return n;
    }
    let list = [];
    list[0] = 0;
    list[1] = 1;
    for (let i = 1; i < n; i++) {
        list[i + 1] = list[i] + list[i - 1];
    }
    return list[n];
}

数组方法,定义一个数组,第零项和第一项的值为0,1,第三位的值等于前两位相加,依次放入数组。

  • 基于 ES6 Generator 实现
function* fib(n) {
        if (n < 0) throw new Error('输入的数字不能小于0');
        let [f0, f1] = [1, 1]
        let count = 0;
        if (n === 0) {
            yield 0
        } else {
            while (count < n) {
                yield f0;
                [f0, f1] = [f1, f0 + f1];
                count++;
            }
        }

    }
    // 0 1 1 2 3
    console.log([...fib(0)]);
    console.log([...fib(1)]);
    console.log([...fib(2)]);
    console.log([...fib(3)]);
    console.log([...fib(4)]);

数组排序

冒泡排序

  • 随便从数组中拿一位数和后一位比较,如果是想从小到大排序,那么就把小的那一位放到前面,大的放在后面,简单来说就是交换它们的位置,如此反复的交换位置就可以得到排序的效果。

    function sortA(arr) {
        for (var i = 0; i < arr.length; i++) {
            for (var j = i; j < arr.length; j++) {
                if (arr[i] > arr[j]) {
                    // 因为需要交换值,所以会把后一个值替换,我们要先保存下来
                    var index = arr[j];
                    // 交换值
                    arr[j] = arr[i];
                    arr[i] = index;
                }
            }
        }
        return arr;
    }
    

快速排序

  • 从数组的中间拿一个值,然后通过这个值挨个和数组里面的值进行比较,如果大于的放一边,小于的放一边,然后把这些合并,再进行比较,如此反复即可。

    function sortB(arr) {
      // 如果只有一位,就没有必要比较
      if (arr.length <= 1) {
        return arr;
      }
      // 获取中间值的索引
      var len = Math.ceil(arr.length / 2);
      // 截取中间值
      var cur = arr.splice(len, 1);
      // 小于中间值放这里面
      var left = [];
      // 大于的放着里面
      var right = [];
      for (var i = 0; i < arr.length; i++) {
        // 判断是否大于
        if (cur > arr[i]) {
          left.push(arr[i]);
        } else {
          right.push(arr[i]);
        }
      }
      // 通过递归,上一轮比较好的数组合并,并且再次进行比较。
      return sortB(left).concat(cur, sortB(right));
    }
    

数组去重

  • 数组去重的方法有很多种,这里说三种:

es5循环写法

定义一个临时存放新数组的变量,循环的时候判断,如果在新数组变量中存在,则不操作,不存在,则push进去。

var arr = ['1', '2', 1, '1', '4', '9', '1'];
function newArr(arr) {
    const tempArr = [];
    for (var i = 0; i < arr.length; i++) {
        if (tempArr.includes(arr[i])) {
            tempArr.push(arr[i]);
        }
    }
    return tempArr;
}
console.log(newArr(arr));

Es6 的 Set 方法

var arr = [1, 1, 2, 9, 6, 9, 6, 3, 1, 4, 5];
function newArr(arr) {
  return Array.from(new Set(arr))
}
console.log(newArr(arr))
  • Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
let arrayLike = {
    0: 'tom', 
    1: '65',
    2: '男',
    'length': 4
}
let arr = Array.from(arrayLike)
console.log(arr) // ['tom','65','男']
  • 要将一个类数组对象转换为一个真正的数组,必须具备以下条件:
  • 该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。
  • 该类数组对象的属性名必须为数值型或字符串型的数字
  • ps: 该类数组对象的属性名可以加引号,也可以不加引号
  • ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。new Set() 返回 Set 对象,因此,可以使用 Array.from 转换为真正的数组。
  • Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如下:
let arr = [12,45,97,9797,564,134,45642]
let set = new Set(arr)
console.log(Array.from(set, item => item + 1)) // [ 13, 46, 98, 9798, 565, 135, 45643 ]
  • 将字符串转换为数组:
let  str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]

Es6 的Map() 方法

  • 利用 new Map 方法的 get 函数和 set 函数,set 将数组的值作为键,1为值。然后用 get 获取键值,如果已经 set 那么键值为1,就不过push到aa里面。
var arr = [1, 1, 2, 9, 6, 9, 6, 3, 1, 4, 5];
function newArr(arr) {
  const tempArr = [];
  const temp = new Map();
  for (let i = 0; i < arr.length; i++) {
    if (!temp.has(arr[i])) {
      temp.set(arr[i], 1);
      tempArr.push(arr[i]);
    }
  }
  return tempArr;
}
console.log(newArr(arr))
  • Set类似于数组,而Map就类似于键值对(Key, Value);ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

多维数组变为一维数组

flat方法

Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

[1, 2, [3, 4]].flat(2)

果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

[1, 2, [3, 4]].flat(Infinity)

toString方法

数组调用toString方法以后,无论多少次数组,都会变成一层的字符串,再调用split就可以转为数组,有个弊端就是,其他数据类型也会被转为字符串

const arr = [1, "2", 'aaaa', [3, [4]]]
function flatten(arr) {
    return arr.toString().split(',')
}

reduce方法

reduce中的返回值为,将上一次运算的结果prev和后面的值使用concat连接,后面的值主要是判断是否为数组,如果为数组,则再调用一次函数,否则返回下一次的值

const arr = [1, "2", 'aaaa', [3, [4]]]
function flatten(arr) {
    return arr.reduce(function (prev, next) {
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

设计模式

单例,工厂,发布订阅

  • 单例模式:在它的核心结构中值包含一个被称为单例的特殊类。一个类只有一个实例,即一个类只有一个对象实例。

  • 工厂模式:在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

  • 发布订阅模式:在软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

其他

深度克隆

以下为深拷贝对象

export const deepCopy = (source) => {
	if (typeof source !== 'object') return source; //如果不是对象和数组的话直接返回
	let target = Array.isArray(source) ? [] : {} //数组兼容
	for (var k in source) {
		// 如果是对象或者数组,就再执行一遍此函数
        target[k] = typeof source[k] === 'object'? deepCopy(source[k]): source[k]
	}
	return target
}
  • 首先判断如果不是对象或者数组,则返回原数据
  • 定义一个空的数组或者对象target来存放拷贝后的数据
  • 使用 for in遍历原数据,前提是要过滤掉原型上的属性。
  • 如果是对象或者数组,则重新执行一遍此函数。
  • 否则则将数据拷贝并放入定义的target变量中

将原生的ajax封装成promise

var  myNewAjax=function({url,data=null}={}){
    return new Promise(function(resolve,reject){
        let xhr = new XMLHttpRequest();
        xhr.open('get',url);
        xhr.send(data);
        xhr.onreadystatechange=function(){
            if(readyState==4){
                if(xhr.status==200){
                    var json=JSON.parse(xhr.responseText);
                    resolve(json)
                }else{
                    reject('error');
                } 
            }
        })
    }

js监听对象属性的改变

我们假设这里有一个user对象,

(1)在ES5中可以通过Object.defineProperty来实现已有属性的监听

Object.defineProperty(user,'name',{
    set:function(key,value){
    }
})

缺点:如果id不在user对象中,则不能监听id的变化

(2)在ES6中可以通过Proxy来实现

var  user = new Proxy({},{
   set:function(target,key,value,receiver){
	}
})

这样即使有属性在user中不存在,通过user.id来定义也同样可以这样监听这个属性的变化哦~

for in 和 for of 的区别

  1. 推荐在循环对象属性的时候,使用for...in,在遍历数组的时候的时候使用for...of
  2. for...in循环出的是key,for...of循环出的是value
  3. 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  4. for...in不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  5. 某些情况下,for...in循环会以任意顺序遍历键名。
  6. for...of 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  7. 不同于forEach方法,它可以与breakcontinuereturn配合使用。
  8. for...of不能循环普通的对象,需要通过和Object.keys()搭配使用,因为普通对象没有部署Iterator接口
  9. for...of提供了遍历所有数据结构的统一操作接口。

普通对象实现Iterator

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

上面代码中,对象jane原生不具备 Iterator 接口,无法用for...of遍历。这时,我们通过 Generator 函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面。

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

实现bind函数

Function.prototype.bind = function (obj) {
    var arg = Array.prototype.slice.call(arguments, 1); // 拿到除了obj以外的其他参数并转换为数组
    var context = this; // 这里的this指向直接调用者,也就是以下面的aa,也就是调用bind函数的函数
    return function () {
        arg = arg.concat(Array.prototype.slice.call(arguments));
        context.apply(obj, arg); // aa调用apply改变this指向obj
    }
}
function aa(a, b, c, d) {
    console.log('this', a, b, c, d, this); 
}
aa.bind(Object, 1, 2)(3, 4) // 打印 1 2 3 4 Object对象