1. 事件冒泡,事件捕获,事件代理

事件冒泡

微软提出的事件流叫事件冒泡,也就是说事件的传播为:从事件开始的具体元素,一级级往上传播到较为不具体的节点。案例如下:

  1. <div id="outer">
  2. <p id="inner">Click me!</p>
  3. </div>

当我们点击P元素时,事件是这样传播的:

  1. p
  2. div
  3. body
  4. html
  5. document 现代浏览器都支持事件冒泡,IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

    事件捕获

    网景团队提出的另一种事件流叫做事件捕获。它的原理刚好和事件冒泡相反,它的用意在于在事件到达预定目标之前捕获它,而最具体的节点应该是最后才接收到事件的。
    比如还是上面的案例,当点击P元素时,事件的传播方向就变成了这样:

  6. document

  7. html
  8. body
  9. div
  10. p

    DOM事件流

    “DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
    首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应。以前面的例子,则会按下图顺序触发事件。
    JS进阶 - 图1
    在DOM事件流中,事件的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到p后就停止了。
    下一个阶段是处于目标阶段,于是事件在p上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回document。

    addEventListener 的第三个参数

    DOM2级事件中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。
    addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。addEventListener有三个参数:
    JS进阶 - 图2

事件代理

在实际的开发当中,利用事件流的特性,我们可以使用一种叫做事件代理的方法。

  1. <ul class="color_list">
  2. <li>red</li>
  3. <li>orange</li>
  4. <li>yellow</li>
  5. <li>green</li>
  6. <li>blue</li>
  7. <li>purple</li>
  8. </ul>
  9. <div class="box"></div>

我们想要在点击每个 li 标签时,输出li当中的颜色(innerHTML) 。常规做法是遍历每个 li ,然后在每个 li 上绑定一个点击事件:

  1. var color_list=document.querySelector(".color_list");
  2. var colors=color_list.getElementsByTagName("li");
  3. var box=document.querySelector(".box");
  4. for(var n=0;n<colors.length;n++){
  5. colors[n].addEventListener("click",function(){
  6. console.log(this.innerHTML)
  7. box.innerHTML="该颜色为 "+this.innerHTML;
  8. })
  9. }

这种做法在 li 较少的时候可以使用,但如果有一万个 li ,那就会导致性能降低(少了遍历所有 li 节点的操作,性能上肯定更加优化)。
这时就需要事件代理出场了,利用事件流的特性,我们只绑定一个事件处理函数也可以完成:

  1. function colorChange(e){
  2. var e=e||window.event;//兼容性的处理
  3. if(e.target.nodeName.toLowerCase()==="li"){
  4. box.innerHTML="该颜色为 "+e.target.innerHTML;
  5. }
  6. }
  7. color_list.addEventListener("click",colorChange,false)

由于事件冒泡机制,点击了 li 后会冒泡到 ul ,此时就会触发绑定在 ul 上的点击事件,再利用 target 找到事件实际发生的元素,就可以达到预期的效果。
使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。假如上述列表元素当中添加了其他的元素节点(如:a、span等),我们不必再一次循环给每一个元素绑定事件,直接修改事件代理的事件处理函数即可。
(1)toLowerCase() 方法用于把字符串转换为小写。语法 stringObject.toLowerCase()
返回值: 一个新的字符串,在其中 stringObject 的所有大写字符全部被转换为了小写字符。
(2)nodeName 属性指定节点的节点名称。如果节点是元素节点,则 nodeName 属性返回标签名。如果节点是属性节点,则 nodeName 属性返回属性的名称。对于其他节点类型,nodeName 属性返回不同节点类型的不同名称。

阻止事件冒泡

1. 给子级加 event.stopPropagation( )

  1. $("#div1").mousedown(function(e){
  2. var e=event||window.event;
  3. event.stopPropagation();
  4. });

阻止默认事件

event.preventDefault( )

2. 柯里化

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

  1. // 普通的add函数
  2. function add(x, y) {
  3. return x + y
  4. }
  5. // currying后
  6. function curryingAdd(x) {
  7. return function (y) {
  8. return x + y
  9. }
  10. }
  11. curryingAdd(1)(2) // 3

作用:

  1. 参数服用 ```javascript function curryingCheck(reg) { return function(txt) { return reg.test(txt) } }

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

hasDigit(‘test1’) // true hasDigit(‘test’) // false hasLetter(‘123’) // false

  1. 2. 提前确认
  2. ```javascript
  3. const on = (document, element, event, handler) => {
  4. if (document.addEventListener) {
  5. element.addEventListener(event, handler, false);
  6. }
  7. else {
  8. element.attachEvent('on' + event, handler);
  9. }
  10. };
  11. // 提前确认注册事件的方法,就不用在每次调用on时候确认
  12. const on = (doc => {
  13. return doc.addEventListener
  14. ? (element, event, handler) => {
  15. element.addEventListener(event, handler, false);
  16. }
  17. : (element, event, handler) => {
  18. element.attachEvent('on' + event, handler);
  19. }
  20. })(document);
  1. 延迟运行 ```javascript function add(…args) { return args.reduce((prev, current) => prev + current); }

add(1, 2, 3, 4);

function curry(fn){ let args = []; return function cb() { if(arguments.length < 1) { return fn(…args); } else { args = […args,…arguments] return cb; } } }

cAdd = curry(add);

cAdd(1)(2)(3)(4)();

  1. 延迟执行实际上就是当我们调用这个方法时,不会立即执行,或者说在参数符合规定的时候才会执行我们真正想执行的内容。<br />**实现:**
  2. 1. 通用实现
  3. ```javascript
  4. function currying(fn, ...rest) {
  5. return function(...args) {
  6. return fn(...rest, ...args);
  7. }
  8. }
  9. function sum(a, b, c, d) {
  10. console.log(a + b + c + d)
  11. }
  12. const add = currying(sum, 1, 2);
  13. add(3, 4);
  14. // 执行结果
  15. 10
  1. 递归实现 ```javascript function add(a, b, c, d) { console.log(a + b + c + d); }

const curriedAdd = currying(add);

curriedAdd(1)(2)(3)(4); // 10 curriedAdd(1, 2, 3)(4); // 10 curriedAdd(1, 2, 3, 4); // 10

function currying(fn) { const len = fn.length; let _args = []; const curry = () => { return function (…args) { // 如果参数攒够了就执行 if (_args.length + args.length >= len) { const result = fn(…_args, …args); // 执行完重置_args _args = []; return result; } // 参数不够就继续攒 else { _args = […_args, …args]; return curry(); } } } return curry(); }

  1. <a name="UEolI"></a>
  2. # 3. 异步编程 promise
  3. <a name="YthQ7"></a>
  4. ## promise
  5. <a name="f8xE8"></a>
  6. ### 简介
  7. promise目的:异步编程解决回调地狱,让程序开发者编写的异步代码具有更好的可读性。<br />promise规范规定了一种异步编程解决方案的API。规范规定了promise对象的状态和then方法。<br />promise是这种异步编程的解决方案的具体实现。
  8. 状态特性用来让使用promise的用户可以及时通知promise任务执行结果。<br />then特性让使用promise的用户可以控制执行完一个任务后执行下一个任务。<br />(使用回调进行异步编程的话,都是用户手动控制的,使用promise的话,只需要告诉promise:“我要执行什么任务”、“我执行的任务结束了”、“然后我要做什么”)<br />[https://www.icode9.com/content-4-365156.html](https://www.icode9.com/content-4-365156.html)<br />浏览器实现并未完全遵照规范
  9. <a name="E1uzO"></a>
  10. ### promise语法
  11. <a name="aFOdy"></a>
  12. #### promise对象
  13. new Promise对象时候传入函数,函数立即执行,函数接收resolve、reject参数,调用resolve或reject时候会改变promise状态。状态改变后不会再变化。
  14. promise状态: pending fullfilled rejected
  15. 未调用resolve或者reject时候处于pending状态,调用resolve后处于fullfilled状态,调用reject后处于rejected状态。如果在pending状态时候,执行任务抛出错误,则变成reject状态。 <br />状态变化后,会执行通过then注册的回调。执行顺序和调用then方法的顺序相同。 <br />调用then方法时候,如果状态是pending则注册回调,等到状态改变时候执行,如果状态已经改变则执行相应的回调。
  16. ```javascript
  17. const p = new Promise((resolve, reject) => {
  18. resolve('test');
  19. });
  20. p.then(
  21. data => console.log(1, 'resolve', data),
  22. data => console.log(1, 'reject', data)
  23. );
  24. p.then(
  25. data => console.log(2, 'resolve', data),
  26. data => console.log(2, 'reject', data)
  27. );
  28. // 执行结果
  29. 1 "resolve" "test"
  30. 2 "resolve" "test"
  1. const p = new Promise((resolve, reject) => {
  2. throw new Error('test-error');
  3. // 由于抛出错误,promise状态已经改变为rejected,再调用resolve将不会改变promise状态
  4. resolve('test');
  5. });
  6. p.then(
  7. data => console.log(1, 'resolve', data),
  8. data => console.log(1, 'reject', data)
  9. );
  10. p.then(
  11. data => console.log(2, 'resolve', data),
  12. data => console.log(2, 'reject', data)
  13. );
  14. // 执行结果
  15. 1 "reject" Error: test-error
  16. 2 "reject" Error: test-error
  1. const p = new Promise((resolve, reject) => {
  2. setTimeout(() => {
  3. resolve('test');
  4. }, 1000);
  5. });
  6. p.then(
  7. data => console.log(1, 'resolve', data),
  8. data => console.log(1, 'reject', data)
  9. );
  10. // 执行结果
  11. 1s后打印 `1 "resolve" "test"`

promise对象的方法:then、catch、all、race

then
then方法接受两个参数,onFulfilled(状态变为fullfilled的回调)和onRejected(状态变为rejected的回调)。返回一个新的promise对象,返回的promise对象的状态与then的参数(onFulfilled、onRejected)和onFulfilled、onRejected方法中返回的值有关。
1. then方法不传参数
如果不传参数,则then方法返回的promise和调用then的promise的状态一致。
更具体地,如果没有onFullfilled参数并且promise的状态为fullfilled,那么then方法返回的promise和调用then方法的promise状态一致;如果没有onRejected参数并且promise状态为rejected,那么then方法返回的promise和调用then方法的promise状态一致。
可以简单地理解:如果上一个promise不处理,那就下一个promise处理。

  1. var p = new Promise(resolve => {
  2. throw new Error('test');
  3. });
  4. p
  5. .then(
  6. () => {}
  7. )
  8. .then(
  9. data => console.log('resolve', data),
  10. err => console.log('reject', err)
  11. );
  12. // 执行结果
  13. reject Error: test
  1. var p = new Promise(resolve => {
  2. resolve('test');
  3. });
  4. p
  5. .then(
  6. undefined, () => {}
  7. )
  8. .then(
  9. data => console.log('resolve', data),
  10. err => console.log('reject', err)
  11. );
  12. // 执行结果
  13. resolve test

2. 回调不返回值
无论onFullfilled中还是onRejected中,不返回值(即默认返回undefined),则then返回的新promise的状态变为fullfilled,值为undefined。

  1. var p = new Promise(resolve => {
  2. resolve('test');
  3. });
  4. p
  5. .then(
  6. () => {}
  7. )
  8. .then(
  9. data => console.log('resolve', data),
  10. err => console.log('reject', err)
  11. );
  12. // 执行结果
  13. resolve undefined
  1. var p = new Promise(resolve => {
  2. throw new Error('test');
  3. });
  4. p
  5. .then(
  6. () => {},
  7. () => {}
  8. )
  9. .then(
  10. data => console.log('resolve', data),
  11. err => console.log('reject', err)
  12. );
  13. // 执行结果
  14. resolve undefined

3. 返回普通值
无论onFullfilled中还是onRejected中,返回普通值,则then返回的新promise的状态变为fullfilled,值为这个值。普通值指的是,非promise对象、非thenable对象(含有then方法的对象)。

  1. var p = new Promise(resolve => {
  2. resolve('test');
  3. });
  4. p
  5. .then(
  6. () => {return 'a'},
  7. () => {return {b: 1}}
  8. )
  9. .then(
  10. data => console.log('resolve', data),
  11. err => console.log('reject', err)
  12. );
  13. // 执行结果
  14. resolve a
  1. var p = new Promise(resolve => {
  2. throw new Error('test');
  3. });
  4. p
  5. .then(
  6. () => {return 'a'},
  7. () => {return {b: 1}}
  8. )
  9. .then(
  10. data => console.log('resolve', data),
  11. err => console.log('reject', err)
  12. )
  13. // 执行结果
  14. resolve {b: 1}

4. 返回promise
无论onFullfilled中还是onRejected中,返回一个promise对象,则以该promise的任务和状态返回新的promise。

  1. var p = new Promise(resolve => {
  2. throw new Error('test');
  3. });
  4. p
  5. .then(
  6. () => {},
  7. () => {return Promise.resolve('yes');}
  8. )
  9. .then(
  10. data => console.log('resolve', data),
  11. err => console.log('reject', err)
  12. );
  13. // 执行结果
  14. resolve yes
  1. var p = new Promise(resolve => {
  2. resolve('test');
  3. });
  4. p
  5. .then(
  6. () => {return Promise.reject('error');},
  7. () => {return {a: 1}}
  8. )
  9. .then(
  10. data => console.log('resolve', data),
  11. err => console.log('reject', err)
  12. );
  13. // 执行结果
  14. reject error

5. 抛出错误
无论onFullfilled中还是onRejected中,抛出错误,则以rejected为状态返回新promise。

  1. var p = new Promise(resolve => {resolve('test')});
  2. p
  3. .then(
  4. () => {throw new Error('1')},
  5. e => {return true}
  6. )
  7. .then(
  8. data => console.log('resolve', data),
  9. e => {console.error('reject', e)}
  10. );
  11. // 执行结果
  12. reject Error: 1
  1. var p = new Promise((r) => {throw new Error('test')});
  2. p
  3. .then(
  4. () => {return true},
  5. e => {throw new Error('2')}
  6. )
  7. .then(
  8. data => console.log('resolve', data),
  9. e => {console.error('reject', e)}
  10. );
  11. // 执行结果
  12. reject Error: 2

catch
catch方法和then方法的reject回调用法相同,如果这时候任务处于rejected状态,则直接执行catch,catch的参数就是reject的reason;如果任务处于pending状态,则注册catch回调,等到状态变成rejected时候再执行。【阮一峰教程示例】

Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数

——《ES6入门教程》 阮一峰

  1. p.then((val) => console.log('fulfilled:', val))
  2. .catch((err) => console.log('rejected', err));
  3. // 等同于
  4. p.then((val) => console.log('fulfilled:', val))
  5. .then(null, (err) => console.log("rejected:", err));

all
Promise.all方法用于多个异步任务执行,当所有任务都正常完成时候,再做后面处理的场景。
Promise.all方法接收一个promise数组作为参数,返回一个promise,当参数的数组中的所有promise都resolve时候,返回的promise才会resolve;而若有一个参数的数组中的promise reject,返回的promise就会reject。
Promise.all方法返回的promise的then的第一个参数onFullfilled回调的参数也是一个数组,对应参数中的数组promise resolve的结果。

  1. const p1 = Promise.resolve(1);
  2. const p2 = new Promise(resolve => {
  3. setTimeout(() => {
  4. resolve(2);
  5. }, 1000);
  6. });
  7. Promise.all([p1, p2])
  8. .then(
  9. ([result1, result2]) => {console.log('resolve', result1, result2);}
  10. );
  11. // 执行结果
  12. resolve 1 2
  1. const p1 = Promise.reject(1);
  2. const p2 = new Promise(resolve => {
  3. setTimeout(() => {
  4. resolve(2);
  5. }, 1000);
  6. });
  7. Promise.all([p1, p2])
  8. .then(
  9. ([result1, result2]) => {console.log('resolve', result1, result2);},
  10. e => console.log('reject', e)
  11. );
  12. // 执行结果
  13. reject 1

race
Promise.race方法用于多个异步任务执行,当有其中一个任务完成或失败时候,就执行后续处理的场景。
Promise.race接收一个promise数组作为参数,返回一个新的promise。当参数数组中其中一个promise resolve或者reject,返回的promise就相应地改变状态。

  1. var p1 = Promise.reject(1);
  2. var p2 = new Promise(resolve => {
  3. setTimeout(() => {
  4. resolve(2);
  5. }, 1000);
  6. });
  7. Promise.race([p1, p2])
  8. .then(
  9. data => {console.log('resolve', data);},
  10. e => {console.log('reject', e);}
  11. );
  12. // 执行结果
  13. reject 1

allSettled
Promise.allSettled用于多个异步任务都结束(完成或者失败)时候,再执行后续任务的场景。
Promise.allSettled接收一个promise数组作为参数,返回一个promise。当参数数组中所有promise状态改变后,返回的promise变为fullfilled状态。
返回的promise的onFullfilled参数接收一个结果数组作为参数,数组对应Promise.allSettled传入的promise数组。结果数组每个元素是一个对象,格式固定:{status, value, reason},标识状态、resolve返回值、reject原因。

  1. var p1 = Promise.reject(1);
  2. var p2 = new Promise(resolve => {
  3. setTimeout(() => {
  4. resolve(2);
  5. }, 1000);
  6. });
  7. Promise.allSettled([p1, p2])
  8. .then(
  9. data => {console.log('resolve', data);},
  10. );
  11. // 执行结果
  12. resolve [{status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]

async/await

Promise虽然解决了回调地狱问题,但是缺点是有不少的样板代码,并且写代码时候还是通过then注册回调方式
async、await是语法糖,可以让开发者以写同步代码的形式写异步逻辑。
语法
如果方法中有await,方法需要加async修饰符。await后面跟一个promise。await表达式结果是promise resolve的值。

  1. const task = () => {
  2. return new Promise(resolve => {
  3. setTimeout(() => {
  4. console.log('1');
  5. resolve('2');
  6. }, 1000);
  7. });
  8. };
  9. async function test() {
  10. console.log(0);
  11. const res = await task();
  12. console.log(res);
  13. }
  14. test();
  15. // 执行结果
  16. 0
  17. 1
  18. 2

async方法返回一个promise。其resolve的值就是async方法中return的值。

  1. async function task1() {
  2. return 'test';
  3. }
  4. task1()
  5. .then(console.log);
  6. // 执行结果
  7. test

如果await后面返回的promise reject掉,需要用try catch语句捕获这个reject

  1. const task = () => {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. reject('test-reject');
  5. }, 1000);
  6. });
  7. };
  8. async function test() {
  9. try {
  10. const res = await task();
  11. }
  12. catch (e) {
  13. console.log('error', e);
  14. }
  15. }
  16. test();
  17. // 执行结果
  18. error test-reject

4. js数据类型

JavaScript类型介绍

数据类型分类

根据《JavaScript高级程序设计》中说明,JavaScript有6种数据类型,Undefined、Null、Boolean、Number、String、Object。但实际上typeof null的值是"object",另外typeof function() {}的值是"function"。因此我们认为null并不是一个独立的类型,null是object类型是一个值,而function也是一个独立的类型。
js数据类型有8种:

  • number
  • string
  • boolean
  • object
  • undefined
  • Symbol
  • BigInt
  • null

undefined类型的值只有一个,就是undefined。
数值类型有两个特殊的值,NaN(not a number)和Infinity(无穷大)
object类型又可以分为

  • plain object // 普通对象
  • Date // 日期
  • Array // 数组
  • RegExp // 正则

其中number、string、boolean、undefined是值类型,function和object是引用类型。

值类型和引用类型

值类型和引用类型的区别是,值类型赋的变量直接存储数据,引用类型的变量存储数据的引用。

  1. let a = 1;
  2. let b = a;
  3. b = 2;
  4. console.log(a, b); // 1, 2
  5. let c = {attr: 'yes'};
  6. let d = c;
  7. d.attr = 'no';
  8. console.log(c.attr, d.attr); // no no
  1. function test(arg) {
  2. arg = 2;
  3. }
  4. let a = 1;
  5. // 相当于将a的值赋给test中的参数变量,参数改变并不会影响到a
  6. test(a);
  7. console.log(a); // 1
  8. function update(arg) {
  9. arg.attr = 2;
  10. }
  11. let b = {attr: 1};
  12. // 将b的引用赋给update的参数变量,参数变量改变引用指向的数据,也会影响到b
  13. update(b);
  14. console.log(b.attr); // 2

包装类型

基础类型的数据在使用时候,js引擎会先将之包装为对象,语句执行完对象被销毁。这个过程也被称为“装箱拆箱”。例如
const arr = '1,2,3'.split(',');
字符串先包装为String对象,然后对象执行相应方法,语句执行完后,包装对象就被销毁。
再如(1).toString()将返回数据类型的包装对象转换成的字符串。
注意:1.toString()会将”.”解析为小数点,因此会报语法错误
包装类型机制扩展了基本数据类型的能力,方便了日常开发。
因为基础类型也有包装类型转为对象,因此除了Symbol都有构造函数。

  1. "1".constructor // 返回函数 String() { [native code] }
  2. (1).constructor // 返回函数 Number() { [native code] }
  3. false.constructor // 返回函数 Boolean() { [native code] }
  4. [1,2,3].constructor // 返回函数 Array() { [native code] }
  5. {}.constructor // 返回函数 Object() { [native code] }
  6. new Date().constructor // 返回函数 Date() { [native code] }
  7. function () {}.constructor // 返回函数 Function(){ [native code] }

null和undefined的区别

本身都表示“没有”,但null表示引用类型的对象为空,undefined则表示变量未定义。
在相等判断时候,null和undefined是相等的。
但null和undefined在很多方面有区别。

含义不同

null表示对象空指针,undefined表示变量未定义。

类型不同

  1. typeof null // 'object'
  2. typeof undefined // 'undefined'
  3. Number(null) // 0
  4. Number(undefined) // NaN

应用场景不同

null

作为对象原型链的终点。

undefined

定义了变量,没有初始化,默认是undefined。
函数不return,或者return后面没有值,则函数默认返回undefined。
函数参数如果不传,默认是undefined。

类型判断

判断类型的方法

typeof

typeof用来查看字面量或者变量的数据类型

  1. typeof 1 // 'number'
  2. typeof '1' // 'string'
  3. typeof false // 'boolean'
  4. typeof {} // 'object'
  5. typeof [] // 'object'
  6. typeof new Date() // 'object'
  7. typeof (() => {}) // 'function'
  8. typeof undefined // 'undefined'
  9. typeof Symbol(1) // 'symbol'

由结果可知typeof可以测试出numberstringbooleanSymbolundefinedfunction,而对于null数组对象,typeof均检测出为object,不能进一步判断它们的类型。

instanceof

instanceof可以判断一个对象的构造函数是否等于给定的值

  1. ({}) instanceof Object // true
  2. [] instanceof Array // true
  3. new Date() instanceof Date // true
  4. /123/g instanceof RegExp // true

instanceof方法一般用于判断自定义构造函数实例。

  1. function Person() {}
  2. const p = new Person();
  3. p instanceof Person // true
  4. p instanceof Object // true

instanceof原理是判断构造函数的原型prototype属性是否出现在对象的原型链上。
需要注意的是,这里提到instanceof是判断对象的构造函数,不适用与非对象类型的变量,看MDN的例子:

  1. let literalString = 'This is a literal string';
  2. let stringObject = new String('String created with constructor');
  3. literalString instanceof String; // false, string literal is not a String
  4. stringObject instanceof String; // true

constructor

  1. console.log(false.constructor === Boolean);// true
  2. console.log((1).constructor === Number);// true
  3. console.log(''.constructor === String);// true
  4. console.log([].constructor === Array);// true
  5. console.log(({}).constructor === Object);// true
  6. console.log((function test() {}).constructor === Function);// true
  7. console.log(Symbol('1').constructor === Symbol);// true

注意:undefined和null没有contructor属性
这里可以看到虽然数字1的构造函数是Number,但1是对象字面量,不是通过new创建的,因此使用instanceof判断为false。

Object.prototype.toString

Object是js中所有其他数据类型的父类。意思是所有的数据类型都继承了Object。但是无论是string还是array都是会重写这个tostring方法的。所以'1'.toString()Object.prototype.toString.call('1')的结果不同。
Object.prototype.toString.call可以用来区分数组、null等引用类型。

  1. function Test(){};
  2. const t = new Test();
  3. Object.prototype.toString.call(1); '[object Number]'
  4. Object.prototype.toString.call(NaN); '[object Number]'
  5. Object.prototype.toString.call('1'); '[object String]'
  6. Object.prototype.toString.call(true); '[object Boolean]'
  7. Object.prototype.toString.call(undefined); '[object Undefined]'
  8. Object.prototype.toString.call(null); '[object Null]'
  9. Object.prototype.toString.call(Symbol());'[object Symbol]'
  10. Object.prototype.toString.call(Test); '[object Function]'
  11. Object.prototype.toString.call([1,2,3]); '[object Array]'
  12. Object.prototype.toString.call({});'[object Object]'
  13. Object.prototype.toString.call(t);'[object Object]'

注意自定义对象的判断只能得到”[object Object]”的结果。

常见变量的类型判断

判断一个变量是否是对象

  1. Object.prototype.toString.call(obj) ==='[object Object]'

判断JavaScript对象是否为空对象

  1. // 方法1 注意该方法性能较差
  2. function isEmptyObject(obj) {
  3. return JSON.stringify(obj) === '{}';
  4. }
  5. // 方法2 因为for in只能枚举对象自身的属性,不能枚举原型属性,因此可以用来判断空对象
  6. function isEmptyObject(obj) {
  7. for (var key in obj) {
  8. return false;
  9. }
  10. return true;
  11. }
  12. // 方法3 Object.keys也是只能获取自身属性,不能获取原型属性
  13. function isEmptyObject(obj) {
  14. return Object.keys(obj).length === 0;
  15. }

如何判断一个对象是否数组

  1. // ES6中增加的数组方法
  2. Array.isArray()
  3. // 使用constructor判断
  4. function isArray(arr) {
  5. return arr.constructor.toString().indexOf("Array") > -1;
  6. }
  7. function isArray(arr) {
  8. return arr.constructor === Array;
  9. }
  10. // 用instanceof判断
  11. function isArray(arr) {
  12. return arr instanceof Array;
  13. }

判断NaN

isNaN()用来判断一个变量是否为NaN

  1. isNaN(NaN); // true

或者利用NaN和自己不相等的特性

  1. typeof num === 'number' && num !== num

类型转换

为什么要做类型转换

js是弱类型的语言,声明变量时候未指定变量类型,因此在很多场景下需要做类型转换。
js和其他端交互(如服务端、native、DOM(如input的value是字符串,需要转换为数字))时候,其他端对数据类型可能有要求
不同类型数据之间可能要进行运算
某些场景支持的数据类型固定(如if的condition需要是bool),这时候需要进行类型转换。
类型转换分为显式转换(包装类型函数、parseInt )和自动转换(隐式转换)。

转换为字符串类型

使用toString方法或者String()效果相同

  1. (1).toSting(); // '1'
  2. String(1); // '1'

null和undefined没有toString方法,其他的类型都有。null 和undefined转换字符串可以用String(null)或者’’ + null

转换为数值类型

parseInt和parseFloat对字符串解析会将字符串前面符合数字规则的部分解析成数字,如果开头就不是数字则返回NaN

  1. parseInt(123); // 123
  2. parseInt('123'); // 123
  3. parseInt('123a'); // 123
  4. parseInt('a123'); // NaN
  5. parseInt('123.123'); // 123
  6. parseFloat('123.123'); // 123.123

解析数组,解析第一个元素。其他情况都返回NaN。

  1. parseInt([]); // NaN
  2. parseInt([1]); // 1
  3. parseInt([1, 2]); // 1
  4. parseInt(['1']); // 1
  5. parseInt(['a']); // NaN

类型转换规则

常见变量转换表

原始值 转为数字 转为字符串 转为布尔
false 0 “false” false
true 1 “true” true
0 0 “0” false
1 1 “1” true
“0” 0 “0” true
“000” 0 “000” true
“1” 1 “1” true
NaN NaN “NaN” false
Infinity Infinity “Infinity” true
-Infinity -Infinity “-Infinity” true
“” 0 “” false
“20” 20 “20” true
“a” NaN “a” true
[] 0 “” true
[10,20] NaN “10,20” true
function(){} NaN “function(){}” true
{ } NaN “[object Object]” true
null 0 “null” false
undefined NaN “undefined” false

对象转原始类型

  • 如果有Symbol.toPrimitive()方法,优先调用再返回
  • 调用valueOf(),如果转换为原始类型,则返回
  • 调用toString(),如果转换为原始类型,则返回
  • 没有Symbol.toPrimitive/valueOf/toString的情况
    • 转布尔值
      • null转为false
      • 非null转为true
    • 转数字NaN
    • 转字符串’[object Object]’ ```javascript // Symbol.toPrimitive var obj = { [Symbol.toPrimitive] () { return 3; }, valueOf () { return 2; }, toString () { return 1; } } console.log(obj) // 3

// valueOf var obj = { valueOf () { return 2; }, toString () { return 1; } } console.log(obj) // 2

// toString var obj = { toString () { return 1; } } console.log(obj) // 1

// 默认 var obj = { } console.log(!!{}); // true console.log(obj + ‘’) // “[object Object]” console.log(+obj); // NaN

  1. <a name="NjIDs"></a>
  2. ### 数组转为原始类型
  3. - 转成bool永远是true
  4. - 转成字符串,用逗号将各个元素连接起来
  5. - 转成数值,先转成字符串,再将字符串转成数值类型
  6. 另一个例子:<br />如何让if (a == 1 && a == 2)返回true
  7. ```javascript
  8. var a = {
  9. value: 0,
  10. valueOf: function() {
  11. this.value++;
  12. return this.value;
  13. }
  14. };
  15. console.log(a == 1 && a == 2);//true

隐式转换

在一些场景中,不同类型的变量会放在一起处理,这时候js引擎会做隐式转换转,转换为相同的类型后再处理。还有些情况下对变量的类型有要求,而变量如果不符合要求就会进行隐式转换(如if语句要求是bool值,如果是非bool值,会先转换为bool再处理)。

隐式转换场景

  1. 算术运算
  2. 单目运算符+
  3. if条件表达式转换为布尔
  4. !运算符转为布尔
  5. ==比较
  6. 比较运算符>、<、≥、≤

    转换规则

  • 双目+号
    • 如果两个操作数都是数值,执行常规的加法计算
    • 如果两个操作数是数值或者布尔,则都转为数值进行计算
    • 如果有至少一个操作数是字符串,则都转成两个字符串拼接
    • 如果有一个操作数是对象,则将这个对象调用toString()转为字符串进行计算
  • -*/号都转换成数字。注意NaN和任何变量运算结果为NaN
  • if转换为bool。
  • !转换为bool。
  • 单目+会转为数字,转换失败时候会转为NaN。
  • ===如果类型不同返回false,如果类型相同则比较值是否相同,注意引用类型对象只和自身相等。
  • == 转换规则
    • 如果是类型相同,直接进行===比较
    • 如果类型不同,要进行转换再比较
      • 如果有一个操作数是布尔,则在比较前先将其转为数值(true -> 1; false -> 0)
      • 数值和字符串比较,比较前先将字符串转为数字
      • 如果一个是对象,另一个不是,则先调用对象的valueOf方法,用得到的基本类型值按照前面的规则比较。
      • null和undefined相等
      • 比较之前,null和undefined不转换为其他任何值(所以null只与自己和undefined==,而且null只和自己===;undefined也是一样)
      • 如果有一个是NaN,则相等返回false,不等返回false
      • 如果两个都是对象,则如果它们是同一个对象返回true,否则返回false
  • 比较运算符> < >= <=转换规则
    • 如果两个操作数都是数值,直接比较
    • 如果只有一个是数值,将另一个操作数转为数值,然后比较
    • 如果两个操作数都是字符串,则按字典比较
    • 如果有一个是bool,则将这个转为数值再比较
    • 如果有一个操作数是对象,则调用它的valueOf()获取原始值,若没有valueOf()方法,则调用toString()方法得到字符串,然后按照之前的规则进行比较。 ```javascript 1 + ‘1’ // ‘11’ 1 + + ‘1’ // 2 1 + 1 + ‘1’ // ‘21’ 1 + ‘1’ + 1 // ‘111’ 1 + ‘a’ // ‘1a’
      1 + +’a’ // NaN 1 - ‘1’ // 0

!![] // true !!’’ // false !!{} // true

if ([]) {console.log(‘bingo’)} // ‘bingo’ if (‘’) {console.log(‘bingo’)} // if (‘0’) {console.log(‘bingo’)} // ‘bingo’ if (‘{}’) {console.log(‘bingo’)} // ‘bingo’

NaN == ‘’ // false NaN == 0 // false NaN == ‘NaN’ // false

1 == ‘1’ // true 0 == ‘0’ // true 0 == ‘’ // true

true == ‘1’ // true true == ‘true’ // false true == ‘0’ // false

[] == true // false [] == ![] // true (首先这个表达式等同于[] == false,然后布尔转为数字:[] == 0,然后对象要转为字符串再比较,即:’’ == 0,这样是一个字符串和一个数值比较,要先将字符串转为数字,即:0 == 0) [] == ‘0’ // false [] == ‘’ // true ({}) == ‘[object Object]’ // true ({}) == 0 // false ({}) == NaN // false

true > ‘0’ // true ‘1.2.3’ < ‘1.2.4’ // true [2] > 1 // true [2, 1] > 1 // false

  1. <a name="HRp9k"></a>
  2. # 5. js继承
  3. <a name="QIMdR"></a>
  4. ## 继承的概念
  5. 谈到继承首先应该说明一下类和对象的概念。类是拥有共通属性和行为的实体的抽象,而对象是一个具体的实例。例如下面这个类Person(人):
  6. ```javascript
  7. function Person(name, age) {
  8. this.name = name;
  9. this.age = age;
  10. this.sayHello = function() {
  11. console.log('hello');
  12. }
  13. }
  14. const p1 = new Person('张三', 11);
  15. const p2 = new Person('李四', 26);

所有的人都有姓名、年龄和讲话的属性,因此Person是对人的一个抽象。张三、李四分别是两个具体的人的实例,即两个对象。他们有不同的名字属性和年龄属性。
js中通过构造函数来实现类。使用new操作符调用函数创建对象时候,这个函数就是一个构造函数。
一个对象的属性包括两种:函数和普通属性,对于普通属性而言,当然是不同实例有自己的属性,就像张三李四不同人有不同的姓名和年龄。而对于函数属性,所有的实例都是一样的。因此大部分情况,类的普通属性应该作为私有属性,而函数属性作为原型属性
继承是建立在面向对象基础上的一种代码复用方式,子类通过继承来复用父类的代码。
由于es6之前,js的构造函数式面向对象语法与传统面向对象编程语言有所区别,js并未在语法层面支持继承的操作,因此js需要通过原型链特性、call apply等的应用来实现继承。

方式

js常用继承方式主要有6种:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承
创造一个超类型的构造函数Super(),为它设置静态属性name、原型链方法getSuper()

  1. function Super(){
  2. this.name =["super"];
  3. }
  4. Super.prototype.getSuper = function(){
  5. return this.name;
  6. }

再创造一个子类构造函数Sub(),使用以上6种继承方法让Sub()继承Super()

  1. function Sub(){}
  • 原型链继承(将子类的原型对象指向超类型的实例) ```javascript Sub.prototype = new Super(); //将Sub的原型对象Sub.prototype指向Super的实例

var sub1 =new Sub(); //创建Sub的实例sub1 sub1.name.push(“sub1”);

var sub2 =new Sub(); //创建Sub的实例sub2 sub2.name.push(“sub2”);

console.log(sub2.getSuper())//[“super”, “sub1”, “sub2”]

  1. 这样可以在Sub中继承 Super的属性name以及原型链方法getSuper,然而在sub1中修改name时,sub2name也会受到影响<br />这种继承方式的缺点是:<br />(1)所有实例共享父类中的属性和方法(如果new父类时传参,则属性也都是一样的)。<br />(2)子类的实例不能向父类型构造函数传参
  2. - **构造函数继承(子类中使用call调用超类)**
  3. ```javascript
  4. function Sub(name){
  5. Super.call(this, name); //在Sub中使用call去调用Super
  6. }
  7. var sub1 = new Sub("Tom");
  8. console.log(sub1.getSuper()) //Uncaught TypeError(不能继承原型链方法)
  9. console.log(sub1.name)//Tom
  10. var sub2 =new Sub();
  11. console.log(sub2.name) //undefined
  12. var sup =new Super()
  13. console.log(sup.getSuper())//undefined

在Sub中用call调用Super,继承了Super的所有静态属性。在实例sub1、sub2中,各自对name的修改也互不影响,实现了属性不共享,子类的实例也能向超类型构造函数传参
这种继承方式的缺点是:
(1)不能继承原型链方法

  • 组合继承(原型链继承+构造函数继承)函数式继承 ```javascript function Sub(name){ Super.call(this, name); //第二次调用父类构造函数,构造函数继承 } Sub.prototype = new Super(); //第一次调用,原型链继承 Sub.prototype.constructor = Sub;

var sub1 =new Sub(“Tom”); console.log(sub1.getSuper()) //Tom console.log(sub1.name) //Tom console.log(sub1 instanceof Sub) //true console.log(sub1 instanceof Super) //true

var sub2 =new Sub(); console.log(sub2.name) //undefined

  1. 在子类Sub中,使用 call继承超类型的属性 + 原型链继承原型链的方法和属性,弥补了上面两种继承方式的三个缺点<br />这种继承方式的缺点是:<br />(1)调用了两次父类的构造函数<br />第一次:Sub.prototype = new Super(),调用一次超类型构造函数<br />第二次:Sub内使用call方法,又调用了一次超类型构造函数,且之后每次实例化子类sub1sub2...的过程中( new Sub() ),都会调用超类型构造函数
  2. - **原型式继承**(创造了一个临时的构造函数F,将 F的原型指向传进来的对象参数,再返回F的实例)
  3. ```javascript
  4. function object(o){
  5. function F(){}
  6. F.prototype = o;
  7. return new F();
  8. }
  9. var person ={
  10. name:"Nicholas",
  11. friends:["Sherlly","Van"],
  12. getname:function() {
  13. return this.name;
  14. }
  15. }
  16. var people1 = object(person);
  17. // var people1 = Object.create(person);在传入一个参数的情况下,Object.create()和object()相同
  18. people1.name ="Greg";
  19. people1.friends.push("Rob");
  20. var people2 = object(person);
  21. people2.name ="Linda";
  22. people2.friends.push("Barbie");
  23. console.log(person.name);//Nicholas
  24. console.log(person.friends);//["Sherlly", "Van", "Rob", "Barbie"]

原型式继承和原型链继承类似,区别:前者是完成了一次对对象的浅拷贝,后者是对构造函数进行继承。
注意:ES5的Object.create()在只有一个参数时与这里的object方法是一样的。Object.create()接受两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
缺点也是一致的:属性会被共享

  • 寄生式继承(基于原型式继承的封装)

    1. //原型式继承
    2. function object(o){
    3. function F(){}
    4. F.prototype = o;
    5. return new F();
    6. }
    7. //寄生式继承
    8. function createAnother(o){
    9. let clone = object(o)
    10. clone.sayHi = function(){
    11. console.log("hi")
    12. }
    13. return clone;
    14. }
    15. var person ={
    16. name:"Nicholas",
    17. friends:["Sherlly","Van"]
    18. }
    19. var anotherPerson = createAnother(person);
    20. anotherPerson.sayHi()

    此方法使用较少,本质上可以通过寄生式继承实现子类方法sayHi的复用,后面通过createAnother()创造出来的对象,都拥有sayHi方法

  • 寄生组合式继承

在组合继承中,若需要优化一次调用,那一定是第一次调用:原型链继承,利用原型式继承便可实现
Sub.prototype = new Super(),实质上就是一次对超类型原型对象的拷贝

  1. function object(o){
  2. function F(){}
  3. F.prototype = o;
  4. return new F();
  5. }
  6. function inheritPrototype(subType, superType){
  7. //复制超类型的原型对象:
  8. let clone = object(superType.prototype);
  9. //将构造函数指向子类型:
  10. clone.constructor = subType;
  11. subType.prototype = clone;
  12. }
  13. function Super(name){
  14. this.name = name
  15. }
  16. Super.prototype.getSuper = function() {
  17. return this.name;
  18. }
  19. function Sub(name){
  20. Super.call(this, name);//第二次调用
  21. }
  22. // 优化前:
  23. // Sub.prototype = new Super(); //第一次调用
  24. // Sub.prototype.constructor = Sub;
  25. // 优化后:
  26. inheritPrototype(Sub, Super);
  27. var sub1 = new Sub("Tom");
  28. console.log(sub1.getSuper()) //Tom
  29. console.log(sub1.name) //Tom
  30. console.log(sub1 instanceof Sub) //true
  31. console.log(sub1 instanceof Super) //true
  32. var sub2 =new Sub();
  33. console.log(sub2.name) //undefined

子类对超类型的原型对象的继承,分为以下几个步骤:
(1)封装一个 inheritPrototype 函数
(2)利用object(或Object.create())复制出超类型的原型对象
(3)将原型对象的构造函数指向自身(把名字改成自己的:clone.constructor = subType,constructor相当于一张身份证,身份证上的名字一定得是自己)
(4)将拷贝出来的对象传递给子类的原型对象
结合性记忆:原型链继承+构造函数继承 = 组合继承;为了优化组合继承→原型式继承→寄生式继承→寄生组合式继承

6. js中的this指向

1. 函数作为对象的属性被调用

作为哪个对象的函数调用的,函数中的this就指向哪个对象。如果在全局环境中调用,this指向window(strict模式时候是undefined)

2. 通过call、apply调用

call和apply都是Function.prototype上的方法。
call和apply可以改变this指向。
当函数调用call()和apply()时,函数都会立即执行。
call和apply的第一个参数都是要绑定的对象。
call方法可以传给该函数的参数分别作为自己的多个参数,而apply方法必须将传给该函数的参数合并成一个数组作为自己的一个参数:

  1. const person = {name: 'Sam'};
  2. function log(a, b) {
  3. console.log(this.name, a, b);
  4. }
  5. log.call(person, 1, 2); // Sam 1 2
  6. log.apply(person, [1, 2]); // Sam 1 2

3. bind绑定后的函数

bind可以将函数的this绑定到指定对象,优先级高于call、apply。
bind后返回一个新的函数。
bind时候还可以传入固定参数。

  1. const person = {name: 'Sam'};
  2. function log(a, b) {
  3. console.log(this.name, a, b);
  4. }
  5. const log1 = log.bind(person, 1, 2);
  6. log1(); // Sam 1 2

4. 使用new调用(当做构造函数调用)

如果函数被当做构造函数调用,函数中的this指向的是实例。优先级高于bind

5. 箭头函数

箭头函数中的this指向的是它声明时候所在的函数的this。如果在全局声明,那this指向window。
箭头函数的优先级最高,使用call或者apply不会改变指向,使用bind的话,也不会改变this指向,但是bind指定的参数会传给函数。

题目

  1. function showThis () {
  2. console.log(this)
  3. }
  4. function showStrictThis () {
  5. 'use strict'
  6. console.log(this)
  7. }
  8. showThis() // window
  9. showStrictThis() // undefined
  1. var boss = {
  2. name: 'boss',
  3. returnThis () {
  4. return this
  5. }
  6. }
  7. boss.returnThis() === boss // true
  1. var boss1 = {
  2. name: 'boss1',
  3. returnThis () {
  4. return this
  5. }
  6. }
  7. var boss2 = {
  8. name: 'boss2',
  9. returnThis () {
  10. return boss1.returnThis()
  11. }
  12. }
  13. var boss3 = {
  14. name: 'boss3',
  15. returnThis () {
  16. var returnThis = boss1.returnThis
  17. return returnThis()
  18. }
  19. }
  20. boss1.returnThis() // boss1
  21. boss2.returnThis() // boss1
  22. boss3.returnThis() // window
  1. var boss1 = {
  2. name: 'boss1',
  3. returnThis () {
  4. return this
  5. }
  6. }
  7. var boss2 = {
  8. name: 'boss2',
  9. returnThis: boss1.returnThis
  10. }
  11. boss2.returnThis() //boss2
  1. function returnThis () {
  2. return this
  3. }
  4. var boss1 = { name: 'boss1' }
  5. returnThis() // window
  6. returnThis.call(boss1) // boss1
  7. returnThis.apply(boss1) // boss1
  1. function returnThis () {
  2. return this
  3. }
  4. var boss1 = { name: 'boss1'}
  5. var boss1returnThis = returnThis.bind(boss1)
  6. boss1returnThis() // boss1
  7. var boss2 = { name: 'boss2' }
  8. boss1returnThis.call(boss2) // still boss1
  1. function showThis () {
  2. console.log(this)
  3. }
  4. showThis() // window
  5. new showThis() // showThis
  6. var boss1 = { name: 'boss1' }
  7. showThis.call(boss1) // boss1
  8. new showThis.call(boss1) // TypeError call is not a constructor call不能被用作构造函数
  9. var boss1showThis = showThis.bind(boss1)
  10. boss1showThis() // boss1
  11. new boss1showThis() // showThis
  1. function callback (cb) {
  2. cb()
  3. }
  4. callback(() => { console.log(this) }) // window
  5. var boss1 = {
  6. name: 'boss1',
  7. callback: callback,
  8. callback2 () {
  9. callback(() => { console.log(this) })
  10. }
  11. }
  12. boss1.callback(() => { console.log(this) }) // still window
  13. boss1.callback2() // boss1
  1. var returnThis = () => this
  2. returnThis() // window
  3. new returnThis() // TypeError
  4. var boss1 = {
  5. name: 'boss1',
  6. returnThis () {
  7. var func = () => this
  8. return func()
  9. }
  10. }
  11. returnThis.call(boss1) // still window
  12. var boss1returnThis = returnThis.bind(boss1)
  13. boss1returnThis() // still window
  14. boss1.returnThis() // boss1
  15. var boss2 = {
  16. name: 'boss2',
  17. returnThis: boss1.returnThis
  18. }
  19. boss2.returnThis() // boss2

7. 作用域、变量提升和闭包

1. 作用域

作用域是可访问变量的集合或者说范围(例如全局的范围、函数的范围、语句块的范围),在作用域内,变量可访问,在作用域外变量不可访问。例如

  1. function test() {
  2. var name = 'test';
  3. console.log('inner', name);
  4. }
  5. test();
  6. console.log('outer', name);

test函数内部可以访问到变量name,而外部则访问不到。
作用域也可以理解为引擎查找变量的规则,js引擎执行代码,访问变量时候,引擎会按照规则查找该变量,如果能找到则执行相应的操作,找不到则报错。
确定变量访问范围的阶段的角度,可以分为2类,词法作用域和动态作用域,js是词法作用域。
变量查找的范围的角度,分为3类,全局作用域,函数作用域和块级作用域。
下面介绍不同的作用域类型。

词法作用域和动态作用域

词法作用域是在词法分析阶段就确定的作用域,变量的访问范围仅由声明时候的区域决定。动态作用域则是在调用时候决定的,它是基于调用栈的。

  1. var a = 2;
  2. function foo() {
  3. console.log( a );
  4. }
  5. function bar() {
  6. var a = 3;
  7. foo();
  8. }
  9. bar();

如果处于词法作用域,也就是现在的javascript环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2。
如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3。

作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止,因此如果内部和外部具有同名的标识符,内部的会被首先查找到,从而“遮蔽”外面的,这叫做“遮蔽效应”。

普通的函数中的this指向有动态作用域的特性,和调用时的对象有关。而箭头函数则使用词法作用域规则。箭头函数的this,就在定义箭头函数的范围内寻找,具体地说,就是外层最近的一个非箭头函数内,或者语句块内,或者全局。看下面的示例:

  1. var name = 'win';
  2. const obj = {
  3. name: 'obj',
  4. a: () => {
  5. console.log(this.name);
  6. }
  7. };
  8. obj.a();

我看可以看到,obj.a声明时候,外层就是全局作用域,因此this指向window。

全局作用域、函数作用域和块级作用域

js有三种作用域:全局作用域、函数作用域和块级作用域(es6)。

全局作用域

直接编写在 script 标签之中的JS代码,或者是一个单独的 JS 文件中的,都是全局作用域。全局作用域在页面打开时创建,页面关闭时销毁。在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。

函数作用域

JavaScript的函数作用域是指在函数内部声明的变量,在函数内部和函数内部声明的函数中都可以访问到。访问变量时候先在函数内部找,找不到则在外层函数中找,直到最外层的全局作用域,形成“作用域链”。
变量在函数内部可访问的含义是,在函数内部的语句中或者函数内部声明的函数中都可以访问,比如

  1. function outer() {
  2. var name = 'outer';
  3. console.log(name); // outer
  4. function inner() {
  5. console.log(name); // outer
  6. }
  7. inner();
  8. }
  9. outer();

函数outer内部定义了变量name,在outer内部可以访问,在outer内部定义的inner也可以访问到。
在访问变量时候,先在当前函数作用域内寻找是否有该变量,如果有则使用之,如果没有则向上寻找上层函数的作用域,一直到全局作用域,如果都没有,则报错。

  1. function outer() {
  2. var name = 'outer';
  3. console.log(name); // outer
  4. function inner() {
  5. var name = 'inner';
  6. console.log(name); // inner
  7. }
  8. inner();
  9. }
  10. outer();

块级作用域

(关于块级作用域详细内容,请参考let和const ——《ECMAScript 6 入门》
变量只在语句块内可访问。通过const和let关键字创建的变量都是在声明的语句块内才可访问。

  1. function test() {
  2. if (true) {
  3. const variable = 'test';
  4. console.log(variable); // test
  5. }
  6. console.log(variable); // Error: variable is not defined
  7. }
  8. test();

块级作用域有几个特性:不存在变量提升、暂时性死区、不允许重复声明
不存在变量提升:

  1. // var 的情况
  2. console.log(foo); // 输出undefined
  3. var foo = 2;
  4. // let 的情况
  5. console.log(bar); // 报错ReferenceError
  6. let bar = 2;

暂时性死区:
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

  1. var tmp = 123;
  2. if (true) {
  3. tmp = 'abc'; // ReferenceError
  4. let tmp;
  5. }

不允许重复声明:

  1. // 报错
  2. function func() {
  3. let a = 10;
  4. var a = 1;
  5. }
  6. // 报错
  7. function func() {
  8. let a = 10;
  9. let a = 1;
  10. }

2. 变量提升

概念

JavaScript在执行之前会先进行预编译,主要做两个工作:

  1. 将全局作用域或者函数作用域内所有函数声明提前。
  2. 将全局作用域或者函数作用域内所有var声明的变量提前声明,并赋值为undefined。

这就是变量提升。
注意:

  1. 函数声明可以提升,但是函数表达式不提升,具名的函数表达式的标识符也不会提升。
  2. 同名的函数声明,后面的覆盖前面的。
  3. 函数声明的提升,不受逻辑判断的控制。

    3. 闭包

    函数和函数内部能访问到的变量的总和,就是一个闭包。
    如何生成闭包?函数内嵌套函数,并且函数执行完后,内部函数会被引用,这样内部函数可以访问外部函数中定义的变量,于是就生成了一个闭包。
    下面是一个闭包的例子: ```javascript function outer() { var a = 1; function inner() {
    1. console.log(a);
    } return inner; }

var b = outer();

  1. 注意,如果没有将outer()执行结果赋值给b,那么内部函数不会被引用,因此没有形成闭包。如果把inner挂在window下面也形成了对inner的引用,也可以生成闭包:
  2. ```javascript
  3. function outer() {
  4. var a = 1;
  5. function inner() {
  6. console.log(a);
  7. }
  8. window.inner = inner;
  9. }
  10. outer();

闭包的作用是什么?可以让内部的函数访问到外部函数的变量,避免变量在全局作用域中存在被修改的风险。
比如我们要实现一个计数器,支持增加计数和获取计数的功能。计数器使用方法如下

  1. var counter = createCounter();
  2. counter.increase(); // +1
  3. console.log(counter.getCount()); /

我们首先可以想到,全局作用域的变量在函数内部可以访问到,所以可以这样实现

  1. var count = 0;
  2. function createCounter() {
  3. function increase() {
  4. count++;
  5. }
  6. function getCount() {
  7. return count;
  8. }
  9. return {
  10. increase: increase,
  11. getCount: getCount
  12. };
  13. }
  14. var counter = createCounter();
  15. counter.increase();
  16. console.log(counter.getCount());
  17. console.log(count);

但是变量count放在全局,很容易被其他模块修改从而导致不可预知的问题。因此我们希望count变量不会被其他模块访问到,于是需要把count放在函数作用域中:

  1. function createCounter() {
  2. var count = 0;
  3. function increase() {
  4. count++;
  5. }
  6. function getCount() {
  7. return count;
  8. }
  9. return {
  10. increase: increase,
  11. getCount: getCount
  12. };
  13. }
  14. var counter = createCounter();
  15. counter.increase();
  16. console.log(counter.getCount());
  17. console.log(count);

这样函数createCounter中的increate和getCount两个函数可以访问到createCounter内部定义的count,这样就形成了闭包。而count只能被createCounter内部定义的函数访问到,因此不会有被随意修改的风险。
通常情况下函数中定义的变量在函数执行完成后会被销毁,例如:

function createCounter() {
    var count = 0;

    function increase() {
        count++;
    }

    function getCount() {
        return count;
    }

    return {
        increase: increase,
        getCount: getCount
    };
}

createCounter();

通常执行完createCounter()方法之后,内部的所有变量都被从内存中销毁(因为没有其他地方使用了)。但是如果生成了闭包(即有对内部嵌套函数的引用),则内部变量不会被销毁(因为还有其他地方在用,嵌套的内部函数还在使用),还是以上面createCounter闭包为例

function createCounter() {
    var count = 0;
    function increase() {
        count++;
    }

    function getCount() {
        return count;
    }

    return {
        increase: increase,
        getCount: getCount
    };
}

var counter = createCounter();
counter.increase();

console.log(counter.getCount());
console.log(count);

由于createCounter返回的方法们被引用,因此形成闭包,所以内部变量count不会被销毁,而是会继续被increase和getCount使用。
生成闭包之后,如果我们不再需要使用counter可以执行counter = null;这样失去了对内部嵌套函数的引用,浏览器就会将方法内资源都销毁调了。因此当我们使用完闭包之后如果后续不再需要使用,最好通过取消引用来释放闭包的资源。
总结:

  1. 什么是闭包?函数和函数内部能访问到的变量的总和,就是一个闭包。
  2. 如何生成闭包? 函数嵌套 + 内部函数被引用。
  3. 闭包作用?隐藏变量,避免放在全局有被篡改的风险。
  4. 使用闭包的注意事项?不用的时候解除引用,避免不必要的内存占用。
  5. 闭包的缺点:使用时候不注意的话,容易产生内存泄漏。

    8. 原型链和原型链继承

    面向对象

    构造函数和对象

    js可以通过构造函数创建对象:

    function Test() {}
    const t = new Test();
    

    Test被称为“类”或者“构造函数”。Test是t的构造函数。
    也可以定义字面量对象

    const a = {};
    // 等价于
    const a = new Object();
    

    a的构造函数是Object。
    我们自己定义构造函数时候,可以给对象添加属性

    function Test(name) {
     this.name = name;
    }
    const t1 = new Test('t1');
    console.log(t1.name); // t1
    

    new的原理

    使用new操作符调用构造函数,做了下面这些工作:

  6. 创建了一个空的js对象(即{})

  7. 将空对象的原型proto指向构造函数的原型
  8. 将空对象作为构造函数的上下文(改变this指向)
  9. 判断构造函数的返回值,以决定最终返回的结果。
    1. 如果返回值是基础数据类型,则忽略返回值;
    2. 如果返回值是引用数据类型,则使用return 的返回,也就是new操作符无效;

所以,任何一个构造函数的原型都是Object的实例。

function Test() {}
console.log(Test.prototype instanceof Object); // true

原型链

使用构造函数生成的对象,和构造函数之间有一些关系:
(1) 构造函数有个prototype对象(原型),该对象有个“constructor”属性,指向构造函数
(2) 每个对象都有一个“**proto”属性,指向它的构造函数的“prototype”属性
(3) 构造函数的prototype对象,也有一个“__
proto**”对象,它指向Object的prototype对象
见图示
image.png
当我们访问对象中的属性时候,会先访问该对象中的本身的属性(私有属性),如果访问不到,会查找对象的“
proto__”指向的构造函数的prototype对象,如果其中有要访问的属性,就使用该值,否则继续访问prototype的“**proto**”,在其中查找要访问属性。这样一直上溯到Object对象。这个就是“原型链”。
原型链提供了继承的能力。

继承

继承用来实现代码复用,即子类继承父类的属性和方法,就实现了父类代码的复用。
使用原型链如何实现继承呢?如果我们有一个父类Parent,Parent有一个方法,打印自己的name属性。

function Parent() {
    this.showName = function () {
        console.log(this.name);
    }
}

如果我们希望子类可以继承父类的方法,如何实现?通过前面介绍的原理,子类对象查找属性时候如果在自己私有属性中访问不到,会到它构造函数的prototype属性中查找,那么我们给子类构造函数的prototype赋值为父类对象,就可以让子类也可以访问父类的方法“showName”了。

function Child() {
    this.name = 'child';
}
Child.prototype = new Parent();

const c = new Child();
c.showName();

当然使用原型链继承无法给父类传递参数,所以在实际应用中,需要结合其他继承方法。

9. JavaScript事件循环

单线程和异步

为什么JavaScript引擎是单线程的?为什么JavaScript要引入异步编程?这些都是老生常谈,不做赘述。值得说明的是,js的事件循环正是用来实现异步特性的。

事件循环概述

js的执行机制就是事件循环。js在执行时,有以下几个主要部分参与了事件循环。

  1. 执行栈,用来创建执行环境,执行js代码
  2. 异步处理模块,用来处理异步事件
  3. 任务队列,包括宏任务队列和微任务队列,用来控制消息的生产消费

事件循环的过程为:当执行栈空的时候,就会从任务队列中,取任务来执行。共分3步:

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

其中,UI渲染会根据浏览器的逻辑,决定要不要马上执行更新,不一定在本次循环中立即执行。可以看到,事件循环中包含UI渲染,这就是为什么我们说js的执行会阻塞UI渲染。

任务队列

任务队列里保存的,是事件的回调函数,异步处理模块就负责将异步事件回调插入到任务队列中。比如,定时器模块执行setTimeout方法后,会在计时结束后,将回调函数加入到相应的宏任务队列中去。

宏任务队列和微任务队列

  1. 宏任务队列可以有多个,微任务队列只有一个
  2. 宏任务有 script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering。微任务有 process.nextTick, Promise, Object.observer, MutationObserver。
  3. 宏任务队列有优先级之分。每次js引擎从宏任务队列中取宏任务时,会按照优先级选择宏任务队列,若高优先级的宏任务队列中没有任务时,才会到低级的宏任务队列中去取任务。
  4. 为什么会有宏任务和微任务之分呢?个人理解,宏任务才是真正意义上的任务,而微任务相当于宏任务的附属的一系列操作和相应。所以,js引擎每次取出一个宏任务执行,并且执行相关的微任务,这样保证一个完整的任务被执行完。这也是微任务队列只有一个的原因,微任务队列就是用来辅助宏任务队列的任务的完整执行的。而宏任务队列才是真正意义的任务,任务有优先级之分就很好理解了,因此才有多个宏任务队列,就是为了区分优先级。

    执行过程

    首先,js引擎从宏任务队列中取任务,因为script(全局)宏任务队列的优先级最高,因此先将这个任务取出(这个任务是当前所有可执行脚本),并利用执行栈执行这些脚本。在执行过程中,会产生一些异步操作,将之交由异步处理模块处理。异步处理模块会产生一系列异步回调,加入到任务队列中。当前执行栈执行完(栈为空)时,再执行微任务队列中的所有任务。执行完后,再从根据优先级从宏任务队列中取出一个任务执行,然后再从微任务队列中取任务执行直到微任务队列为空。 就这样循环往复,生生不息。