yshaojun / blog

1 stars 1 forks source link

学习 JavaScript 的三大门槛 #2

Open yshaojun opened 5 years ago

yshaojun commented 5 years ago

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.jscore.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,需要先记住如下几点:

  1. Promise 是一个拥有 then 方法的对象(thenable 对象);
  2. then 方法返回的仍然是一个 Promise(所以可以连写多个 then);

下一条就很重要了:

  1. 如果上一个 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 代码就在眼前。