1. 事件冒泡,事件捕获,事件代理
事件冒泡
微软提出的事件流叫事件冒泡,也就是说事件的传播为:从事件开始的具体元素,一级级往上传播到较为不具体的节点。案例如下:
<div id="outer">
<p id="inner">Click me!</p>
</div>
当我们点击P元素时,事件是这样传播的:
- p
- div
- body
- html
document 现代浏览器都支持事件冒泡,IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。
事件捕获
网景团队提出的另一种事件流叫做事件捕获。它的原理刚好和事件冒泡相反,它的用意在于在事件到达预定目标之前捕获它,而最具体的节点应该是最后才接收到事件的。
比如还是上面的案例,当点击P元素时,事件的传播方向就变成了这样:document
- html
- body
- div
- p
DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应。以前面的例子,则会按下图顺序触发事件。
在DOM事件流中,事件的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到p后就停止了。
下一个阶段是处于目标阶段,于是事件在p上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回document。addEventListener 的第三个参数
DOM2级事件中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。
addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。addEventListener有三个参数:
事件代理
在实际的开发当中,利用事件流的特性,我们可以使用一种叫做事件代理的方法。
<ul class="color_list">
<li>red</li>
<li>orange</li>
<li>yellow</li>
<li>green</li>
<li>blue</li>
<li>purple</li>
</ul>
<div class="box"></div>
我们想要在点击每个 li 标签时,输出li当中的颜色(innerHTML) 。常规做法是遍历每个 li ,然后在每个 li 上绑定一个点击事件:
var color_list=document.querySelector(".color_list");
var colors=color_list.getElementsByTagName("li");
var box=document.querySelector(".box");
for(var n=0;n<colors.length;n++){
colors[n].addEventListener("click",function(){
console.log(this.innerHTML)
box.innerHTML="该颜色为 "+this.innerHTML;
})
}
这种做法在 li 较少的时候可以使用,但如果有一万个 li ,那就会导致性能降低(少了遍历所有 li 节点的操作,性能上肯定更加优化)。
这时就需要事件代理出场了,利用事件流的特性,我们只绑定一个事件处理函数也可以完成:
function colorChange(e){
var e=e||window.event;//兼容性的处理
if(e.target.nodeName.toLowerCase()==="li"){
box.innerHTML="该颜色为 "+e.target.innerHTML;
}
}
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( )
$("#div1").mousedown(function(e){
var e=event||window.event;
event.stopPropagation();
});
阻止默认事件
2. 柯里化
柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 ——维基百科
// 普通的add函数
function add(x, y) {
return x + y
}
// currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
curryingAdd(1)(2) // 3
作用:
- 参数服用 ```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
2. 提前确认
```javascript
const on = (document, element, event, handler) => {
if (document.addEventListener) {
element.addEventListener(event, handler, false);
}
else {
element.attachEvent('on' + event, handler);
}
};
// 提前确认注册事件的方法,就不用在每次调用on时候确认
const on = (doc => {
return doc.addEventListener
? (element, event, handler) => {
element.addEventListener(event, handler, false);
}
: (element, event, handler) => {
element.attachEvent('on' + event, handler);
}
})(document);
- 延迟运行 ```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)();
延迟执行实际上就是当我们调用这个方法时,不会立即执行,或者说在参数符合规定的时候才会执行我们真正想执行的内容。<br />**实现:**
1. 通用实现
```javascript
function currying(fn, ...rest) {
return function(...args) {
return fn(...rest, ...args);
}
}
function sum(a, b, c, d) {
console.log(a + b + c + d)
}
const add = currying(sum, 1, 2);
add(3, 4);
// 执行结果
10
- 递归实现 ```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(); }
<a name="UEolI"></a>
# 3. 异步编程 promise
<a name="YthQ7"></a>
## promise
<a name="f8xE8"></a>
### 简介
promise目的:异步编程解决回调地狱,让程序开发者编写的异步代码具有更好的可读性。<br />promise规范规定了一种异步编程解决方案的API。规范规定了promise对象的状态和then方法。<br />promise是这种异步编程的解决方案的具体实现。
状态特性用来让使用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 />浏览器实现并未完全遵照规范
<a name="E1uzO"></a>
### promise语法
<a name="aFOdy"></a>
#### promise对象
new Promise对象时候传入函数,函数立即执行,函数接收resolve、reject参数,调用resolve或reject时候会改变promise状态。状态改变后不会再变化。
promise状态: pending fullfilled rejected
未调用resolve或者reject时候处于pending状态,调用resolve后处于fullfilled状态,调用reject后处于rejected状态。如果在pending状态时候,执行任务抛出错误,则变成reject状态。 <br />状态变化后,会执行通过then注册的回调。执行顺序和调用then方法的顺序相同。 <br />调用then方法时候,如果状态是pending则注册回调,等到状态改变时候执行,如果状态已经改变则执行相应的回调。
```javascript
const p = new Promise((resolve, reject) => {
resolve('test');
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
p.then(
data => console.log(2, 'resolve', data),
data => console.log(2, 'reject', data)
);
// 执行结果
1 "resolve" "test"
2 "resolve" "test"
const p = new Promise((resolve, reject) => {
throw new Error('test-error');
// 由于抛出错误,promise状态已经改变为rejected,再调用resolve将不会改变promise状态
resolve('test');
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
p.then(
data => console.log(2, 'resolve', data),
data => console.log(2, 'reject', data)
);
// 执行结果
1 "reject" Error: test-error
2 "reject" Error: test-error
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test');
}, 1000);
});
p.then(
data => console.log(1, 'resolve', data),
data => console.log(1, 'reject', data)
);
// 执行结果
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处理。
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
reject Error: test
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
undefined, () => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve test
2. 回调不返回值
无论onFullfilled中还是onRejected中,不返回值(即默认返回undefined),则then返回的新promise的状态变为fullfilled,值为undefined。
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve undefined
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {},
() => {}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve undefined
3. 返回普通值
无论onFullfilled中还是onRejected中,返回普通值,则then返回的新promise的状态变为fullfilled,值为这个值。普通值指的是,非promise对象、非thenable对象(含有then方法的对象)。
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {return 'a'},
() => {return {b: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve a
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {return 'a'},
() => {return {b: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
)
// 执行结果
resolve {b: 1}
4. 返回promise
无论onFullfilled中还是onRejected中,返回一个promise对象,则以该promise的任务和状态返回新的promise。
var p = new Promise(resolve => {
throw new Error('test');
});
p
.then(
() => {},
() => {return Promise.resolve('yes');}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
resolve yes
var p = new Promise(resolve => {
resolve('test');
});
p
.then(
() => {return Promise.reject('error');},
() => {return {a: 1}}
)
.then(
data => console.log('resolve', data),
err => console.log('reject', err)
);
// 执行结果
reject error
5. 抛出错误
无论onFullfilled中还是onRejected中,抛出错误,则以rejected为状态返回新promise。
var p = new Promise(resolve => {resolve('test')});
p
.then(
() => {throw new Error('1')},
e => {return true}
)
.then(
data => console.log('resolve', data),
e => {console.error('reject', e)}
);
// 执行结果
reject Error: 1
var p = new Promise((r) => {throw new Error('test')});
p
.then(
() => {return true},
e => {throw new Error('2')}
)
.then(
data => console.log('resolve', data),
e => {console.error('reject', e)}
);
// 执行结果
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入门教程》 阮一峰
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.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的结果。
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.all([p1, p2])
.then(
([result1, result2]) => {console.log('resolve', result1, result2);}
);
// 执行结果
resolve 1 2
const p1 = Promise.reject(1);
const p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.all([p1, p2])
.then(
([result1, result2]) => {console.log('resolve', result1, result2);},
e => console.log('reject', e)
);
// 执行结果
reject 1
race
Promise.race方法用于多个异步任务执行,当有其中一个任务完成或失败时候,就执行后续处理的场景。
Promise.race接收一个promise数组作为参数,返回一个新的promise。当参数数组中其中一个promise resolve或者reject,返回的promise就相应地改变状态。
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.race([p1, p2])
.then(
data => {console.log('resolve', data);},
e => {console.log('reject', e);}
);
// 执行结果
reject 1
allSettled
Promise.allSettled用于多个异步任务都结束(完成或者失败)时候,再执行后续任务的场景。
Promise.allSettled接收一个promise数组作为参数,返回一个promise。当参数数组中所有promise状态改变后,返回的promise变为fullfilled状态。
返回的promise的onFullfilled参数接收一个结果数组作为参数,数组对应Promise.allSettled传入的promise数组。结果数组每个元素是一个对象,格式固定:{status, value, reason},标识状态、resolve返回值、reject原因。
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
Promise.allSettled([p1, p2])
.then(
data => {console.log('resolve', data);},
);
// 执行结果
resolve [{status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]
async/await
Promise虽然解决了回调地狱问题,但是缺点是有不少的样板代码,并且写代码时候还是通过then注册回调方式
async、await是语法糖,可以让开发者以写同步代码的形式写异步逻辑。
语法
如果方法中有await,方法需要加async修饰符。await后面跟一个promise。await表达式结果是promise resolve的值。
const task = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log('1');
resolve('2');
}, 1000);
});
};
async function test() {
console.log(0);
const res = await task();
console.log(res);
}
test();
// 执行结果
0
1
2
async方法返回一个promise。其resolve的值就是async方法中return的值。
async function task1() {
return 'test';
}
task1()
.then(console.log);
// 执行结果
test
如果await后面返回的promise reject掉,需要用try catch语句捕获这个reject
const task = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('test-reject');
}, 1000);
});
};
async function test() {
try {
const res = await task();
}
catch (e) {
console.log('error', e);
}
}
test();
// 执行结果
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是引用类型。
值类型和引用类型
值类型和引用类型的区别是,值类型赋的变量直接存储数据,引用类型的变量存储数据的引用。
let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2
let c = {attr: 'yes'};
let d = c;
d.attr = 'no';
console.log(c.attr, d.attr); // no no
function test(arg) {
arg = 2;
}
let a = 1;
// 相当于将a的值赋给test中的参数变量,参数改变并不会影响到a
test(a);
console.log(a); // 1
function update(arg) {
arg.attr = 2;
}
let b = {attr: 1};
// 将b的引用赋给update的参数变量,参数变量改变引用指向的数据,也会影响到b
update(b);
console.log(b.attr); // 2
包装类型
基础类型的数据在使用时候,js引擎会先将之包装为对象,语句执行完对象被销毁。这个过程也被称为“装箱拆箱”。例如const arr = '1,2,3'.split(',');
字符串先包装为String对象,然后对象执行相应方法,语句执行完后,包装对象就被销毁。
再如(1).toString()将返回数据类型的包装对象转换成的字符串。
注意:1.toString()会将”.”解析为小数点,因此会报语法错误
包装类型机制扩展了基本数据类型的能力,方便了日常开发。
因为基础类型也有包装类型转为对象,因此除了Symbol都有构造函数。
"1".constructor // 返回函数 String() { [native code] }
(1).constructor // 返回函数 Number() { [native code] }
false.constructor // 返回函数 Boolean() { [native code] }
[1,2,3].constructor // 返回函数 Array() { [native code] }
{}.constructor // 返回函数 Object() { [native code] }
new Date().constructor // 返回函数 Date() { [native code] }
function () {}.constructor // 返回函数 Function(){ [native code] }
null和undefined的区别
本身都表示“没有”,但null表示引用类型的对象为空,undefined则表示变量未定义。
在相等判断时候,null和undefined是相等的。
但null和undefined在很多方面有区别。
含义不同
类型不同
typeof null // 'object'
typeof undefined // 'undefined'
Number(null) // 0
Number(undefined) // NaN
应用场景不同
null
undefined
定义了变量,没有初始化,默认是undefined。
函数不return,或者return后面没有值,则函数默认返回undefined。
函数参数如果不传,默认是undefined。
类型判断
判断类型的方法
typeof
typeof用来查看字面量或者变量的数据类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof false // 'boolean'
typeof {} // 'object'
typeof [] // 'object'
typeof new Date() // 'object'
typeof (() => {}) // 'function'
typeof undefined // 'undefined'
typeof Symbol(1) // 'symbol'
由结果可知typeof可以测试出number、string、boolean、Symbol、undefined及function,而对于null及数组、对象,typeof均检测出为object,不能进一步判断它们的类型。
instanceof
instanceof可以判断一个对象的构造函数是否等于给定的值
({}) instanceof Object // true
[] instanceof Array // true
new Date() instanceof Date // true
/123/g instanceof RegExp // true
instanceof方法一般用于判断自定义构造函数实例。
function Person() {}
const p = new Person();
p instanceof Person // true
p instanceof Object // true
instanceof原理是判断构造函数的原型prototype属性是否出现在对象的原型链上。
需要注意的是,这里提到instanceof是判断对象的构造函数,不适用与非对象类型的变量,看MDN的例子:
let literalString = 'This is a literal string';
let stringObject = new String('String created with constructor');
literalString instanceof String; // false, string literal is not a String
stringObject instanceof String; // true
constructor
console.log(false.constructor === Boolean);// true
console.log((1).constructor === Number);// true
console.log(''.constructor === String);// true
console.log([].constructor === Array);// true
console.log(({}).constructor === Object);// true
console.log((function test() {}).constructor === Function);// true
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等引用类型。
function Test(){};
const t = new Test();
Object.prototype.toString.call(1); '[object Number]'
Object.prototype.toString.call(NaN); '[object Number]'
Object.prototype.toString.call('1'); '[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(Symbol());'[object Symbol]'
Object.prototype.toString.call(Test); '[object Function]'
Object.prototype.toString.call([1,2,3]); '[object Array]'
Object.prototype.toString.call({});'[object Object]'
Object.prototype.toString.call(t);'[object Object]'
注意自定义对象的判断只能得到”[object Object]”的结果。
常见变量的类型判断
判断一个变量是否是对象
Object.prototype.toString.call(obj) ==='[object Object]'
判断JavaScript对象是否为空对象
// 方法1 注意该方法性能较差
function isEmptyObject(obj) {
return JSON.stringify(obj) === '{}';
}
// 方法2 因为for in只能枚举对象自身的属性,不能枚举原型属性,因此可以用来判断空对象
function isEmptyObject(obj) {
for (var key in obj) {
return false;
}
return true;
}
// 方法3 Object.keys也是只能获取自身属性,不能获取原型属性
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
如何判断一个对象是否数组
// ES6中增加的数组方法
Array.isArray()
// 使用constructor判断
function isArray(arr) {
return arr.constructor.toString().indexOf("Array") > -1;
}
function isArray(arr) {
return arr.constructor === Array;
}
// 用instanceof判断
function isArray(arr) {
return arr instanceof Array;
}
判断NaN
isNaN()用来判断一个变量是否为NaN
isNaN(NaN); // true
或者利用NaN和自己不相等的特性
typeof num === 'number' && num !== num
类型转换
为什么要做类型转换
js是弱类型的语言,声明变量时候未指定变量类型,因此在很多场景下需要做类型转换。
js和其他端交互(如服务端、native、DOM(如input的value是字符串,需要转换为数字))时候,其他端对数据类型可能有要求
不同类型数据之间可能要进行运算
某些场景支持的数据类型固定(如if的condition需要是bool),这时候需要进行类型转换。
类型转换分为显式转换(包装类型函数、parseInt )和自动转换(隐式转换)。
转换为字符串类型
使用toString方法或者String()效果相同
(1).toSting(); // '1'
String(1); // '1'
null和undefined没有toString方法,其他的类型都有。null 和undefined转换字符串可以用String(null)或者’’ + null
转换为数值类型
parseInt和parseFloat对字符串解析会将字符串前面符合数字规则的部分解析成数字,如果开头就不是数字则返回NaN
parseInt(123); // 123
parseInt('123'); // 123
parseInt('123a'); // 123
parseInt('a123'); // NaN
parseInt('123.123'); // 123
parseFloat('123.123'); // 123.123
解析数组,解析第一个元素。其他情况都返回NaN。
parseInt([]); // NaN
parseInt([1]); // 1
parseInt([1, 2]); // 1
parseInt(['1']); // 1
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
<a name="NjIDs"></a>
### 数组转为原始类型
- 转成bool永远是true
- 转成字符串,用逗号将各个元素连接起来
- 转成数值,先转成字符串,再将字符串转成数值类型
另一个例子:<br />如何让if (a == 1 && a == 2)返回true
```javascript
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true
隐式转换
在一些场景中,不同类型的变量会放在一起处理,这时候js引擎会做隐式转换转,转换为相同的类型后再处理。还有些情况下对变量的类型有要求,而变量如果不符合要求就会进行隐式转换(如if语句要求是bool值,如果是非bool值,会先转换为bool再处理)。
隐式转换场景
- 双目+号
- 如果两个操作数都是数值,执行常规的加法计算
- 如果两个操作数是数值或者布尔,则都转为数值进行计算
- 如果有至少一个操作数是字符串,则都转成两个字符串拼接
- 如果有一个操作数是对象,则将这个对象调用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
<a name="HRp9k"></a>
# 5. js继承
<a name="QIMdR"></a>
## 继承的概念
谈到继承首先应该说明一下类和对象的概念。类是拥有共通属性和行为的实体的抽象,而对象是一个具体的实例。例如下面这个类Person(人):
```javascript
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log('hello');
}
}
const p1 = new Person('张三', 11);
const p2 = new Person('李四', 26);
所有的人都有姓名、年龄和讲话的属性,因此Person是对人的一个抽象。张三、李四分别是两个具体的人的实例,即两个对象。他们有不同的名字属性和年龄属性。
js中通过构造函数来实现类。使用new操作符调用函数创建对象时候,这个函数就是一个构造函数。
一个对象的属性包括两种:函数和普通属性,对于普通属性而言,当然是不同实例有自己的属性,就像张三李四不同人有不同的姓名和年龄。而对于函数属性,所有的实例都是一样的。因此大部分情况,类的普通属性应该作为私有属性,而函数属性作为原型属性。
继承是建立在面向对象基础上的一种代码复用方式,子类通过继承来复用父类的代码。
由于es6之前,js的构造函数式面向对象语法与传统面向对象编程语言有所区别,js并未在语法层面支持继承的操作,因此js需要通过原型链特性、call apply等的应用来实现继承。
方式
js常用继承方式主要有6种:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承
创造一个超类型的构造函数Super(),为它设置静态属性name、原型链方法getSuper()
function Super(){
this.name =["super"];
}
Super.prototype.getSuper = function(){
return this.name;
}
再创造一个子类构造函数Sub(),使用以上6种继承方法让Sub()继承Super()
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”]
这样可以在Sub中继承 Super的属性name以及原型链方法getSuper,然而在sub1中修改name时,sub2的name也会受到影响<br />这种继承方式的缺点是:<br />(1)所有实例共享父类中的属性和方法(如果new父类时传参,则属性也都是一样的)。<br />(2)子类的实例不能向父类型构造函数传参
- **构造函数继承(子类中使用call调用超类)**
```javascript
function Sub(name){
Super.call(this, name); //在Sub中使用call去调用Super
}
var sub1 = new Sub("Tom");
console.log(sub1.getSuper()) //Uncaught TypeError(不能继承原型链方法)
console.log(sub1.name)//Tom
var sub2 =new Sub();
console.log(sub2.name) //undefined
var sup =new Super()
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
在子类Sub中,使用 call继承超类型的属性 + 原型链继承原型链的方法和属性,弥补了上面两种继承方式的三个缺点<br />这种继承方式的缺点是:<br />(1)调用了两次父类的构造函数<br />第一次:Sub.prototype = new Super(),调用一次超类型构造函数<br />第二次:Sub内使用call方法,又调用了一次超类型构造函数,且之后每次实例化子类sub1、sub2...的过程中( new Sub() ),都会调用超类型构造函数
- **原型式继承**(创造了一个临时的构造函数F,将 F的原型指向传进来的对象参数,再返回F的实例)
```javascript
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person ={
name:"Nicholas",
friends:["Sherlly","Van"],
getname:function() {
return this.name;
}
}
var people1 = object(person);
// var people1 = Object.create(person);在传入一个参数的情况下,Object.create()和object()相同
people1.name ="Greg";
people1.friends.push("Rob");
var people2 = object(person);
people2.name ="Linda";
people2.friends.push("Barbie");
console.log(person.name);//Nicholas
console.log(person.friends);//["Sherlly", "Van", "Rob", "Barbie"]
原型式继承和原型链继承类似,区别:前者是完成了一次对对象的浅拷贝,后者是对构造函数进行继承。
注意:ES5的Object.create()在只有一个参数时与这里的object方法是一样的。Object.create()接受两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
缺点也是一致的:属性会被共享
寄生式继承(基于原型式继承的封装)
//原型式继承
function object(o){
function F(){}
F.prototype = o;
return new F();
}
//寄生式继承
function createAnother(o){
let clone = object(o)
clone.sayHi = function(){
console.log("hi")
}
return clone;
}
var person ={
name:"Nicholas",
friends:["Sherlly","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi()
此方法使用较少,本质上可以通过寄生式继承实现子类方法sayHi的复用,后面通过createAnother()创造出来的对象,都拥有sayHi方法
寄生组合式继承
在组合继承中,若需要优化一次调用,那一定是第一次调用:原型链继承,利用原型式继承便可实现
Sub.prototype = new Super(),实质上就是一次对超类型原型对象的拷贝
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){
//复制超类型的原型对象:
let clone = object(superType.prototype);
//将构造函数指向子类型:
clone.constructor = subType;
subType.prototype = clone;
}
function Super(name){
this.name = name
}
Super.prototype.getSuper = function() {
return this.name;
}
function Sub(name){
Super.call(this, name);//第二次调用
}
// 优化前:
// Sub.prototype = new Super(); //第一次调用
// Sub.prototype.constructor = Sub;
// 优化后:
inheritPrototype(Sub, Super);
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)封装一个 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方法必须将传给该函数的参数合并成一个数组作为自己的一个参数:
const person = {name: 'Sam'};
function log(a, b) {
console.log(this.name, a, b);
}
log.call(person, 1, 2); // Sam 1 2
log.apply(person, [1, 2]); // Sam 1 2
3. bind绑定后的函数
bind可以将函数的this绑定到指定对象,优先级高于call、apply。
bind后返回一个新的函数。
bind时候还可以传入固定参数。
const person = {name: 'Sam'};
function log(a, b) {
console.log(this.name, a, b);
}
const log1 = log.bind(person, 1, 2);
log1(); // Sam 1 2
4. 使用new调用(当做构造函数调用)
如果函数被当做构造函数调用,函数中的this指向的是实例。优先级高于bind
5. 箭头函数
箭头函数中的this指向的是它声明时候所在的函数的this。如果在全局声明,那this指向window。
箭头函数的优先级最高,使用call或者apply不会改变指向,使用bind的话,也不会改变this指向,但是bind指定的参数会传给函数。
题目
function showThis () {
console.log(this)
}
function showStrictThis () {
'use strict'
console.log(this)
}
showThis() // window
showStrictThis() // undefined
var boss = {
name: 'boss',
returnThis () {
return this
}
}
boss.returnThis() === boss // true
var boss1 = {
name: 'boss1',
returnThis () {
return this
}
}
var boss2 = {
name: 'boss2',
returnThis () {
return boss1.returnThis()
}
}
var boss3 = {
name: 'boss3',
returnThis () {
var returnThis = boss1.returnThis
return returnThis()
}
}
boss1.returnThis() // boss1
boss2.returnThis() // boss1
boss3.returnThis() // window
var boss1 = {
name: 'boss1',
returnThis () {
return this
}
}
var boss2 = {
name: 'boss2',
returnThis: boss1.returnThis
}
boss2.returnThis() //boss2
function returnThis () {
return this
}
var boss1 = { name: 'boss1' }
returnThis() // window
returnThis.call(boss1) // boss1
returnThis.apply(boss1) // boss1
function returnThis () {
return this
}
var boss1 = { name: 'boss1'}
var boss1returnThis = returnThis.bind(boss1)
boss1returnThis() // boss1
var boss2 = { name: 'boss2' }
boss1returnThis.call(boss2) // still boss1
function showThis () {
console.log(this)
}
showThis() // window
new showThis() // showThis
var boss1 = { name: 'boss1' }
showThis.call(boss1) // boss1
new showThis.call(boss1) // TypeError call is not a constructor call不能被用作构造函数
var boss1showThis = showThis.bind(boss1)
boss1showThis() // boss1
new boss1showThis() // showThis
function callback (cb) {
cb()
}
callback(() => { console.log(this) }) // window
var boss1 = {
name: 'boss1',
callback: callback,
callback2 () {
callback(() => { console.log(this) })
}
}
boss1.callback(() => { console.log(this) }) // still window
boss1.callback2() // boss1
var returnThis = () => this
returnThis() // window
new returnThis() // TypeError
var boss1 = {
name: 'boss1',
returnThis () {
var func = () => this
return func()
}
}
returnThis.call(boss1) // still window
var boss1returnThis = returnThis.bind(boss1)
boss1returnThis() // still window
boss1.returnThis() // boss1
var boss2 = {
name: 'boss2',
returnThis: boss1.returnThis
}
boss2.returnThis() // boss2
7. 作用域、变量提升和闭包
1. 作用域
作用域是可访问变量的集合或者说范围(例如全局的范围、函数的范围、语句块的范围),在作用域内,变量可访问,在作用域外变量不可访问。例如
function test() {
var name = 'test';
console.log('inner', name);
}
test();
console.log('outer', name);
test函数内部可以访问到变量name,而外部则访问不到。
作用域也可以理解为引擎查找变量的规则,js引擎执行代码,访问变量时候,引擎会按照规则查找该变量,如果能找到则执行相应的操作,找不到则报错。
从确定变量访问范围的阶段的角度,可以分为2类,词法作用域和动态作用域,js是词法作用域。
从变量查找的范围的角度,分为3类,全局作用域,函数作用域和块级作用域。
下面介绍不同的作用域类型。
词法作用域和动态作用域
词法作用域是在词法分析阶段就确定的作用域,变量的访问范围仅由声明时候的区域决定。动态作用域则是在调用时候决定的,它是基于调用栈的。
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
如果处于词法作用域,也就是现在的javascript环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2。
如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3。
作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止,因此如果内部和外部具有同名的标识符,内部的会被首先查找到,从而“遮蔽”外面的,这叫做“遮蔽效应”。
普通的函数中的this指向有动态作用域的特性,和调用时的对象有关。而箭头函数则使用词法作用域规则。箭头函数的this,就在定义箭头函数的范围内寻找,具体地说,就是外层最近的一个非箭头函数内,或者语句块内,或者全局。看下面的示例:
var name = 'win';
const obj = {
name: 'obj',
a: () => {
console.log(this.name);
}
};
obj.a();
我看可以看到,obj.a声明时候,外层就是全局作用域,因此this指向window。
全局作用域、函数作用域和块级作用域
js有三种作用域:全局作用域、函数作用域和块级作用域(es6)。
全局作用域
直接编写在 script 标签之中的JS代码,或者是一个单独的 JS 文件中的,都是全局作用域。全局作用域在页面打开时创建,页面关闭时销毁。在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。
函数作用域
JavaScript的函数作用域是指在函数内部声明的变量,在函数内部和函数内部声明的函数中都可以访问到。访问变量时候先在函数内部找,找不到则在外层函数中找,直到最外层的全局作用域,形成“作用域链”。
变量在函数内部可访问的含义是,在函数内部的语句中或者函数内部声明的函数中都可以访问,比如
function outer() {
var name = 'outer';
console.log(name); // outer
function inner() {
console.log(name); // outer
}
inner();
}
outer();
函数outer内部定义了变量name,在outer内部可以访问,在outer内部定义的inner也可以访问到。
在访问变量时候,先在当前函数作用域内寻找是否有该变量,如果有则使用之,如果没有则向上寻找上层函数的作用域,一直到全局作用域,如果都没有,则报错。
function outer() {
var name = 'outer';
console.log(name); // outer
function inner() {
var name = 'inner';
console.log(name); // inner
}
inner();
}
outer();
块级作用域
(关于块级作用域详细内容,请参考let和const ——《ECMAScript 6 入门》)
变量只在语句块内可访问。通过const和let关键字创建的变量都是在声明的语句块内才可访问。
function test() {
if (true) {
const variable = 'test';
console.log(variable); // test
}
console.log(variable); // Error: variable is not defined
}
test();
块级作用域有几个特性:不存在变量提升、暂时性死区、不允许重复声明
不存在变量提升:
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区:
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
不允许重复声明:
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
2. 变量提升
概念
JavaScript在执行之前会先进行预编译,主要做两个工作:
- 将全局作用域或者函数作用域内所有函数声明提前。
- 将全局作用域或者函数作用域内所有var声明的变量提前声明,并赋值为undefined。
这就是变量提升。
注意:
- 函数声明可以提升,但是函数表达式不提升,具名的函数表达式的标识符也不会提升。
- 同名的函数声明,后面的覆盖前面的。
- 函数声明的提升,不受逻辑判断的控制。
3. 闭包
函数和函数内部能访问到的变量的总和,就是一个闭包。
如何生成闭包?函数内嵌套函数,并且函数执行完后,内部函数会被引用,这样内部函数可以访问外部函数中定义的变量,于是就生成了一个闭包。
下面是一个闭包的例子: ```javascript function outer() { var a = 1; function inner() {
} return inner; }console.log(a);
var b = outer();
注意,如果没有将outer()执行结果赋值给b,那么内部函数不会被引用,因此没有形成闭包。如果把inner挂在window下面也形成了对inner的引用,也可以生成闭包:
```javascript
function outer() {
var a = 1;
function inner() {
console.log(a);
}
window.inner = inner;
}
outer();
闭包的作用是什么?可以让内部的函数访问到外部函数的变量,避免变量在全局作用域中存在被修改的风险。
比如我们要实现一个计数器,支持增加计数和获取计数的功能。计数器使用方法如下
var counter = createCounter();
counter.increase(); // +1
console.log(counter.getCount()); /
我们首先可以想到,全局作用域的变量在函数内部可以访问到,所以可以这样实现
var count = 0;
function createCounter() {
function increase() {
count++;
}
function getCount() {
return count;
}
return {
increase: increase,
getCount: getCount
};
}
var counter = createCounter();
counter.increase();
console.log(counter.getCount());
console.log(count);
但是变量count放在全局,很容易被其他模块修改从而导致不可预知的问题。因此我们希望count变量不会被其他模块访问到,于是需要把count放在函数作用域中:
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中的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;这样失去了对内部嵌套函数的引用,浏览器就会将方法内资源都销毁调了。因此当我们使用完闭包之后如果后续不再需要使用,最好通过取消引用来释放闭包的资源。
总结:
- 什么是闭包?函数和函数内部能访问到的变量的总和,就是一个闭包。
- 如何生成闭包? 函数嵌套 + 内部函数被引用。
- 闭包作用?隐藏变量,避免放在全局有被篡改的风险。
- 使用闭包的注意事项?不用的时候解除引用,避免不必要的内存占用。
-
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操作符调用构造函数,做了下面这些工作:
创建了一个空的js对象(即{})
- 将空对象的原型proto指向构造函数的原型
- 将空对象作为构造函数的上下文(改变this指向)
- 判断构造函数的返回值,以决定最终返回的结果。
- 如果返回值是基础数据类型,则忽略返回值;
- 如果返回值是引用数据类型,则使用return 的返回,也就是new操作符无效;
所以,任何一个构造函数的原型都是Object的实例。
function Test() {}
console.log(Test.prototype instanceof Object); // true
原型链
使用构造函数生成的对象,和构造函数之间有一些关系:
(1) 构造函数有个prototype对象(原型),该对象有个“constructor”属性,指向构造函数
(2) 每个对象都有一个“**proto”属性,指向它的构造函数的“prototype”属性
(3) 构造函数的prototype对象,也有一个“__proto**”对象,它指向Object的prototype对象
见图示
当我们访问对象中的属性时候,会先访问该对象中的本身的属性(私有属性),如果访问不到,会查找对象的“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在执行时,有以下几个主要部分参与了事件循环。
- 执行栈,用来创建执行环境,执行js代码
- 异步处理模块,用来处理异步事件
- 任务队列,包括宏任务队列和微任务队列,用来控制消息的生产消费
事件循环的过程为:当执行栈空的时候,就会从任务队列中,取任务来执行。共分3步:
- 取一个宏任务来执行。执行完毕后,下一步。
- 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
- 更新UI渲染。
其中,UI渲染会根据浏览器的逻辑,决定要不要马上执行更新,不一定在本次循环中立即执行。可以看到,事件循环中包含UI渲染,这就是为什么我们说js的执行会阻塞UI渲染。
任务队列
任务队列里保存的,是事件的回调函数,异步处理模块就负责将异步事件回调插入到任务队列中。比如,定时器模块执行setTimeout方法后,会在计时结束后,将回调函数加入到相应的宏任务队列中去。
宏任务队列和微任务队列
- 宏任务队列可以有多个,微任务队列只有一个
- 宏任务有 script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering。微任务有 process.nextTick, Promise, Object.observer, MutationObserver。
- 宏任务队列有优先级之分。每次js引擎从宏任务队列中取宏任务时,会按照优先级选择宏任务队列,若高优先级的宏任务队列中没有任务时,才会到低级的宏任务队列中去取任务。
- 为什么会有宏任务和微任务之分呢?个人理解,宏任务才是真正意义上的任务,而微任务相当于宏任务的附属的一系列操作和相应。所以,js引擎每次取出一个宏任务执行,并且执行相关的微任务,这样保证一个完整的任务被执行完。这也是微任务队列只有一个的原因,微任务队列就是用来辅助宏任务队列的任务的完整执行的。而宏任务队列才是真正意义的任务,任务有优先级之分就很好理解了,因此才有多个宏任务队列,就是为了区分优先级。
执行过程
首先,js引擎从宏任务队列中取任务,因为script(全局)宏任务队列的优先级最高,因此先将这个任务取出(这个任务是当前所有可执行脚本),并利用执行栈执行这些脚本。在执行过程中,会产生一些异步操作,将之交由异步处理模块处理。异步处理模块会产生一系列异步回调,加入到任务队列中。当前执行栈执行完(栈为空)时,再执行微任务队列中的所有任务。执行完后,再从根据优先级从宏任务队列中取出一个任务执行,然后再从微任务队列中取任务执行直到微任务队列为空。 就这样循环往复,生生不息。