—类型转换—
引用类型转Boolean均是true
if([])(console.log(1)) //会打出1
if({})(console.log(1)) //会打出1
对象转基本类型
1. 对象在转换基本类型时,会调用 valueOf 和 toString,并且这两个方法你是可以重写的。
2. 调用哪个方法,主要是要看这个对象倾向于转换为什么。如果倾向于转换为 Number 类型的,就优先调用 valueOf;如果倾向于转换为 String 类型,就只调用 toString
var obj = {
toString () {
console.log('toString')
return 'string'
},
valueOf () {
console.log('valueOf')
return 5
}
}
alert(obj) // string
console.log(1 + obj) // 6
如果重写了 toString 方法,而没有重写 valueOf 方法,则会调用 toString 方法
var obj = {
toString () {
return 'string'
}
}
console.log(1 + obj) // 1string
3. 用上述两个方法的时候,需要 return 原始类型的值 (primitive value)
如果在调用 valueOf 的时候,返回的不是原始类型的值,就会去调用 toString
var obj = {
toString () {
console.log('toString')
return 'string'
},
valueOf () {
console.log('valueOf')
return {}
}
}
console.log(1 + obj)
// 依次打印出
valueOf
toString
1string
如果返回还不是原始的值,就会报错
var obj = {
toString () {
console.log('toString')
return {}
},
valueOf () {
console.log('valueOf')
return {}
}
}
console.log(1 + obj)
// 报错。无法将一个对象转换为原始类型的值
Uncaught TypeError: Cannot convert object to primitive value
与上面的结果不同,因为上面的对象的toString被重写,而原始空对象{}的toString仍然输出”[object Object]”。原始空数组的toString输出””。
4. 如果有 Symbol.toPrimitive 属性的话,会优先调用,它的优先级最高
var obj = {
toString () {
console.log('toString')
return {}
},
valueOf () {
console.log('valueOf')
return {}
},
[Symbol.toPrimitive] () {
console.log('primitive')
return 'primi'
}
}
console.log(1 + obj) // 1primi
- 同样只能 return 原始类型的值,否则会报和上面所说一样的错。
var obj = {
toString () {
console.log('toString')
return {}
},
valueOf () {
console.log('valueOf')
return {}
},
[Symbol.toPrimitive] () {
console.log('primitive')
return {}
}
}
console.log(1 + obj)
// 报错
TypeError: Cannot convert object to primitive value
(实测不会再调用toString和valueOf方法)
5. 其他问题
var obj = {
toString () {
console.log('toString')
return '1'
},
valueOf () {
console.log('valueOf')
return 2
}
}
console.log(1 + obj)
console.log('1' + obj)
// 依次输出
valueOf
3
valueOf
12
按理来说
'1' + obj
这个 + 是倾向于转为转换为字符串类型的(也就是调用 toString);但是,它最后却是调用 valueOf。
原因在于,对于 + 这个操作符来说,本身就是代表加法,也就是说,本来就倾向于转为 number 类型,只不过发现加号左边出现字符串,于是乎把它的右边也转为字符串(也就是调用 valueOf 后,return 的值再转为字符串)
—比较运算符中的类型转换—
===
==
对于 ==
来说,如果对比双方的类型不一样的话,就会进行类型转换,这也就用到了我们上一章节讲的内容。
假如我们需要对比 x
和 y
是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否相同。相同的话就是比大小了
- 类型不相同的话,那么就会进行类型转换
- 会先判断是否在对比
null
和undefined
,是的话就会返回true
判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1'
↓
1 == 1
判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断'1' == true
↓
'1' == 1
↓
1 == 1
判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断'1' == { name: 'yck' }
↓
'1' == '[object Object]'
简单总结:不涉及undefined和null时,两原始类型相比,都转数字。原始类型和引用类型相比,转成这个原始类型。两引用类型相比,结果为false。
典型例子:
[] == ! [] -> [] == 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
—this问题—
普通函数中,谁调用该函数,this指向谁。
箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。
使用bind
这些改变上下文的 API ,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是 window
。如果对一个函数进行多次 bind
,this
永远由第一次 bind
决定
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
如果你认为输出结果是 a
,那么你就错了,其实我们可以把上述代码转换成另一种形式
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以从上述代码中发现,不管我们给函数 bind
几次,fn
中的 this
永远由第一次 bind
决定,所以结果永远是 window
。
—原型继承和 Class 继承—
组合继承
组合继承是最常用的继承方式,
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this)
继承父类的属性,然后改变子类的原型为 new Parent()
来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class
去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class
实现继承的核心在于使用 extends
表明继承自哪个父类,并且在子类构造函数中必须调用 super
,因为这段代码可以看成 Parent.call(this, value)
。
当然了,之前也说了在 JS 中并不存在类,class
的本质就是函数。
—模块化—
用模块化可以给我们带来以下好处
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
立即执行函数
在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
AMD 和 CMD
鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。
// AMD
define(['./a', './b'], function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
CommonJS
CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
因为 CommonJS 还是会使用到的,所以这里会对一些疑难点进行解析
先说 require
吧
var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// module 基本实现
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over
另外虽然 exports
和 module.exports
用法相似,但是不能对 exports
直接赋值。因为 var exports = module.exports
这句代码表明了 exports
和 module.exports
享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports
赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports
起效。
ES Module
ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
- CommonJS 支持动态导入,也就是
require(${path}/xx.js)
,后者目前不支持,但是已有提案 - CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- ES Module 会编译成
require/exports
来执行的// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
—setInterval—(时间准确性待确定)
function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () => {
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
startTime = endTime = now()
callback(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}
let a = 0
setInterval(timer => {
console.log(1)
a++
if (a === 3) cancelAnimationFrame(timer)
}, 1000)
—箭头函数—
箭头函数的this不可被bind,call,apply改变
var b=9
var a=()=>{
console.log(this.b)
}
a.call({b:10})
//9
—事件—
通常我们认为 stopPropagation
是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation
同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
事件代理的方式相较于直接给目标注册事件来说,有以下优点:
- 节省内存
- 不需要给子节点注销事件
e.stopPropagation();
e.stopImmediatePropagation(),可以阻挡当前元素的其他绑定事件(同一事件类型)触发,两者都能使该元素绑定的相同事件不再向下触发