学习 JavaScript 的三大门槛
yshaojun opened this issue · comments
JavaScript 与 C-like 语言相比,有着如下3个特有或特别的概念,形成其他语言开发者学习 JavaScript 的三大门槛,经历由 Python 开发转前端的我对此深有体会。
原型继承(prototype)
ES6 之前,JavaScript 是没有 class
关键字的(更别提 extends
),但却有 对象(Object) 的概念,那么如何实现对象间的继承呢?JavaScript 使用一种简单粗暴的方式,直接将要继承的对象挂载在新对象的某个属性上,而这个被继承的对象就叫新对象的 原型(prototype) 。
由于原型也是对象,那它也可以有原型,由此形成所谓的 原型链(prototype chain)。
在语言实现上,访问原型并不需要指明新对象上对应的属性名,当访问一个对象属性时,会先在对象本身查找;如果没有该属性,就在对象的原型上查找;如果原型上也没有,就在原型的原型上查找,一直往上。
下面是典型的定义和使用类方式,如果真的不理解 prototype
,记住这是 JavaScript 定义类的写法也行:
function F (a) {
this.a = a
}
F.prototype.print = function () {
console.log(this.a)
}
const c = new F('hello') // 对象 c 的原型就是 F.prototype
c.print() // output: hello
这里还有另外2个东西需要注意下:
__proto__
:上文提到“将要继承的对象挂载在新对象的某个属性上”,那么这个属性名是什么呢?bingo! 正是 __proto__
。
c.__proto__ === F.prototype // true
constructor
: 一个函数(比如上面的 F
)的 prototype
有个 constructor
属性,默认(可以改)指向函数本身。
F.prototype.constructor === F // true
// 由于 c.__proto__ === F.prototype,所以:
c.constructor === F
判断原型有没过关有个简单的办法:能不能看懂 jQuery 的无 new
构造 jQuery.fn.init
。相关代码 init.js、core.js,如果不熟 AMD 模块,可以去看打包后的 jQuery。
当然,ES6 新增了 class
/extends
关键字,基本覆盖了原型使用场景,原型的概念也在弱化,但是如果想读一些开源库源码,原型知识还是很必要的。
this 指向
C-like 语言只有在类、对象里会出现 this
,但是 JavaScript 的 this
非常灵活,几乎可以在任何位置出现,那么如何准确的判断函数体里 this
指向呢?犀牛书里总结的很好:
函数调用(直接调用):严格模式下指向 undefined
,非严格模式下指向全局对象(浏览器:window
,Node:global
);
方法调用(.
运算符调用):.
指向该对象;
构造调用(new
运算符调用):指向该新创建的对象;
间接调用:bind
/call
/apply
指向传入的对象。
加上 ES6 的箭头函数:
箭头函数:指向定义时所在的对象,而不是使用时所在的对象。
记住这5条,this
指向问题就过关了。
this
本质上仍然是传参,这个参数叫 上下文(context),其实更推荐通过形参传递 context
,使代码清晰易懂:
function f1 (b) {
console.log(this.a + b)
}
f1.bind({ a: 1 })(1) // output: 2
function f2 (ctx, b) {
console.log(ctx.a + b)
}
f2({ a: 1 }, 1) // output: 2
Promise
JavaScript 长期以来都运行在浏览器单线程里,因此天生支持异步,在“回调地狱”被广为诟病的情况下,Promise
方案应运而生。
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('resolved'), 1000)
})
p.then(r => {
console.log(r) // output: resolved
})
理解 Promise
,需要先记住如下几点:
Promise
是一个拥有then
方法的对象(thenable
对象);then
方法返回的仍然是一个Promise
(所以可以连写多个then
);
下一条就很重要了:
- 如果上一个
then
函数里返回的是普通值,那么下一个then
函数里拿到的参数就是这个值;如果上一个then
函数里返回的是Promise
,那么下一个函数里拿到的参数就是这个Promise
resolve 的值。
p.then(r => {
console.log(r) // output: resolved
return 'common value'
}).then(v => {
console.log(v) // output: common value
return new Promise(resolve => {
setTimeout(() => resolve('resolved value'), 1000)
})
}).then(v => {
console.log(v) // output: resolved value
}).then(v => {
console.log(v) // output: undefined
})
顺便提一下 async
/await
语法,也有这样的“规律”:
如果 await
后面跟的是普通值,那么结果就是这个值;如果 await
后面跟的是 Promise
,那么结果就是这个 Promise
resolve 的值。
async function f () {
const t1 = await 'common value'
console.log(t1) // output: common value
const t2 = await new Promise(resolve => {
setTimeout(() => resolve('resolved value'), 1000)
})
console.log(t2) // output: resolved value
return 'result'
}
f().then(r => {
console.log(r) // output: result
})
任何 async
函数的返回值总是一个 Promise
,该 Promise
resolve 的值即是函数的返回值。
判断 Promise
有没有过关也有个简单的办法:使用 fs Promises API 写目录遍历函数,生成一个 json 目录树。
过了这三个槛,其他内容基本和 C-like 语言大同小异了,配合 MDN 文档,精准把握每一行 JavaScript 代码就在眼前。