—类型转换—

image.png

引用类型转Boolean均是true

  1. if([])(console.log(1)) //会打出1
  2. if({})(console.log(1)) //会打出1

对象转基本类型

1. 对象在转换基本类型时,会调用 valueOf 和 toString,并且这两个方法你是可以重写的。

2. 调用哪个方法,主要是要看这个对象倾向于转换为什么。如果倾向于转换为 Number 类型的,就优先调用 valueOf;如果倾向于转换为 String 类型,就只调用 toString

  1. var obj = {
  2. toString () {
  3. console.log('toString')
  4. return 'string'
  5. },
  6. valueOf () {
  7. console.log('valueOf')
  8. return 5
  9. }
  10. }
  11. alert(obj) // string
  12. console.log(1 + obj) // 6

如果重写了 toString 方法,而没有重写 valueOf 方法,则会调用 toString 方法

  1. var obj = {
  2. toString () {
  3. return 'string'
  4. }
  5. }
  6. console.log(1 + obj) // 1string

3. 用上述两个方法的时候,需要 return 原始类型的值 (primitive value)

  • 如果在调用 valueOf 的时候,返回的不是原始类型的值,就会去调用 toString

    1. var obj = {
    2. toString () {
    3. console.log('toString')
    4. return 'string'
    5. },
    6. valueOf () {
    7. console.log('valueOf')
    8. return {}
    9. }
    10. }
    11. console.log(1 + obj)
    12. // 依次打印出
    13. valueOf
    14. toString
    15. 1string
  • 如果返回还不是原始的值,就会报错

  1. var obj = {
  2. toString () {
  3. console.log('toString')
  4. return {}
  5. },
  6. valueOf () {
  7. console.log('valueOf')
  8. return {}
  9. }
  10. }
  11. console.log(1 + obj)
  12. // 报错。无法将一个对象转换为原始类型的值
  13. Uncaught TypeError: Cannot convert object to primitive value

image.png与上面的结果不同,因为上面的对象的toString被重写,而原始空对象{}的toString仍然输出”[object Object]”。原始空数组的toString输出””。

4. 如果有 Symbol.toPrimitive 属性的话,会优先调用,它的优先级最高

  1. var obj = {
  2. toString () {
  3. console.log('toString')
  4. return {}
  5. },
  6. valueOf () {
  7. console.log('valueOf')
  8. return {}
  9. },
  10. [Symbol.toPrimitive] () {
  11. console.log('primitive')
  12. return 'primi'
  13. }
  14. }
  15. console.log(1 + obj) // 1primi
  • 同样只能 return 原始类型的值,否则会报和上面所说一样的错。
  1. var obj = {
  2. toString () {
  3. console.log('toString')
  4. return {}
  5. },
  6. valueOf () {
  7. console.log('valueOf')
  8. return {}
  9. },
  10. [Symbol.toPrimitive] () {
  11. console.log('primitive')
  12. return {}
  13. }
  14. }
  15. console.log(1 + obj)
  16. // 报错
  17. TypeError: Cannot convert object to primitive value

(实测不会再调用toString和valueOf方法)

5. 其他问题

  1. var obj = {
  2. toString () {
  3. console.log('toString')
  4. return '1'
  5. },
  6. valueOf () {
  7. console.log('valueOf')
  8. return 2
  9. }
  10. }
  11. console.log(1 + obj)
  12. console.log('1' + obj)
  13. // 依次输出
  14. valueOf
  15. 3
  16. valueOf
  17. 12

按理来说

  1. '1' + obj

这个 + 是倾向于转为转换为字符串类型的(也就是调用 toString);但是,它最后却是调用 valueOf。
原因在于,对于 + 这个操作符来说,本身就是代表加法,也就是说,本来就倾向于转为 number 类型,只不过发现加号左边出现字符串,于是乎把它的右边也转为字符串(也就是调用 valueOf 后,return 的值再转为字符串)

—比较运算符中的类型转换—

===

不进行类型转换,直接比较自身是否完全相等。

==

对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换,这也就用到了我们上一章节讲的内容。
假如我们需要对比 xy 是否相同,就会进行如下判断流程:

  • 首先会判断两者类型是否相同。相同的话就是比大小了
  • 类型不相同的话,那么就会进行类型转换
  • 会先判断是否在对比 nullundefined,是的话就会返回 true
  • 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number

    1. 1 == '1'
    2. 1 == 1
  • 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断

    1. '1' == true
    2. '1' == 1
    3. 1 == 1
  • 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断

    1. '1' == { name: 'yck' }
    2. '1' == '[object Object]'

简单总结:不涉及undefined和null时,两原始类型相比,都转数字。原始类型和引用类型相比,转成这个原始类型。两引用类型相比,结果为false。

image.png

典型例子:

[] == ! [] -> [] == false -> [] == 0 -> ‘’ == 0 -> 0 == 0 -> true

{} == ! {} -> {} == false -> {} == 0 -> NaN == 0 -> false**
比较时,对象类型会转化成原始类型,若转成数字,则可以理解为对象先转换成字符串,在转换成数字。
所以这两个案例结果不同的主要原因是:
[]转成字符串为””,在转化为数值是0。而{}转成字符串为”[object Object]”,在转化为数值是NaN。

重写Object的原型toString方法后:
Object.prototype.toString=()=>Array.prototype.toString.apply([])
var a={}
{}==!{} //true
image.png

—this问题—

难点%26易混淆点 - 图5
普通函数中,谁调用该函数,this指向谁。
箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this

使用bind 这些改变上下文的 API ,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window。如果对一个函数进行多次 bindthis 永远由第一次 bind 决定

  1. let a = {}
  2. let fn = function () { console.log(this) }
  3. fn.bind().bind(a)() // => ?

如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式

  1. // fn.bind().bind(a) 等于
  2. let fn2 = function fn1() {
  3. return function() {
  4. return fn.apply()
  5. }.apply(a)
  6. }
  7. fn2()

可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

—原型继承和 Class 继承—

组合继承

组合继承是最常用的继承方式,

  1. function Parent(value) {
  2. this.val = value
  3. }
  4. Parent.prototype.getValue = function() {
  5. console.log(this.val)
  6. }
  7. function Child(value) {
  8. Parent.call(this, value)
  9. }
  10. Child.prototype = new Parent()
  11. const child = new Child(1)
  12. child.getValue() // 1
  13. child instanceof Parent // true

以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
难点%26易混淆点 - 图6

寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。

  1. function Parent(value) {
  2. this.val = value
  3. }
  4. Parent.prototype.getValue = function() {
  5. console.log(this.val)
  6. }
  7. function Child(value) {
  8. Parent.call(this, value)
  9. }
  10. Child.prototype = Object.create(Parent.prototype, {
  11. constructor: {
  12. value: Child,
  13. enumerable: false,
  14. writable: true,
  15. configurable: true
  16. }
  17. })
  18. const child = new Child(1)
  19. child.getValue() // 1
  20. child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
难点%26易混淆点 - 图7

Class 继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单

  1. class Parent {
  2. constructor(value) {
  3. this.val = value
  4. }
  5. getValue() {
  6. console.log(this.val)
  7. }
  8. }
  9. class Child extends Parent {
  10. constructor(value) {
  11. super(value)
  12. }
  13. }
  14. let child = new Child(1)
  15. child.getValue() // 1
  16. child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)
当然了,之前也说了在 JS 中并不存在类,class 的本质就是函数。

—模块化—

用模块化可以给我们带来以下好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题

  1. (function(globalVariable){
  2. globalVariable.test = function() {}
  3. // ... 声明各种变量、函数都不会污染全局作用域
  4. })(globalVariable)

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。

  1. // AMD
  2. define(['./a', './b'], function(a, b) {
  3. // 加载模块完毕可以使用
  4. a.do()
  5. b.do()
  6. })
  7. // CMD
  8. define(function(require, exports, module) {
  9. // 加载模块
  10. // 可以把 require 写在函数体的任意地方实现延迟加载
  11. var a = require('./a')
  12. a.doSomething()
  13. })

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。

  1. // a.js
  2. module.exports = {
  3. a: 1
  4. }
  5. // or
  6. exports.a = 1
  7. // b.js
  8. var module = require('./a.js')
  9. module.a // -> log 1

因为 CommonJS 还是会使用到的,所以这里会对一些疑难点进行解析
先说 require

  1. var module = require('./a.js')
  2. module.a
  3. // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
  4. // 重要的是 module 这里,module 是 Node 独有的一个变量
  5. module.exports = {
  6. a: 1
  7. }
  8. // module 基本实现
  9. var module = {
  10. id: 'xxxx', // 我总得知道怎么去找到他吧
  11. exports: {} // exports 就是个空对象
  12. }
  13. // 这个是为什么 exports 和 module.exports 用法相似的原因
  14. var exports = module.exports
  15. var load = function (module) {
  16. // 导出的东西
  17. var a = 1
  18. module.exports = a
  19. return module.exports
  20. };
  21. // 然后当我 require 的时候去找到独特的
  22. // id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exportsmodule.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exportsmodule.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的
    1. // 引入模块 API
    2. import XXX from './a.js'
    3. import { XXX } from './a.js'
    4. // 导出模块 API
    5. export function a() {}
    6. export default function() {}

—setInterval—(时间准确性待确定)

  1. function setInterval(callback, interval) {
  2. let timer
  3. const now = Date.now
  4. let startTime = now()
  5. let endTime = startTime
  6. const loop = () => {
  7. timer = window.requestAnimationFrame(loop)
  8. endTime = now()
  9. if (endTime - startTime >= interval) {
  10. startTime = endTime = now()
  11. callback(timer)
  12. }
  13. }
  14. timer = window.requestAnimationFrame(loop)
  15. return timer
  16. }
  17. let a = 0
  18. setInterval(timer => {
  19. console.log(1)
  20. a++
  21. if (a === 3) cancelAnimationFrame(timer)
  22. }, 1000)

—箭头函数—

箭头函数的this不可被bind,call,apply改变

  1. var b=9
  2. var a=()=>{
  3. console.log(this.b)
  4. }
  5. a.call({b:10})
  6. //9

—事件—

通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

事件代理的方式相较于直接给目标注册事件来说,有以下优点:

  • 节省内存
  • 不需要给子节点注销事件

image.png
image.pnge.stopPropagation();
image.pnge.stopImmediatePropagation(),可以阻挡当前元素的其他绑定事件(同一事件类型)触发,两者都能使该元素绑定的相同事件不再向下触发