if (true) {
let name = ‘布兰’
}
console.log(name) // undefined
const/let
不存在变量提升,所以在代码块里必须先声明然后才可以使用,这叫暂时性死区;
let name = ‘bubuzou’
if (true) {
name = ‘布兰’
let name
}
console.log(name)
const/let
不允许在同一个作用域内,重复声明;
function setName(name) {
let name = ‘’ // SyntaxError
}
const
声明时必须初始化,且后期不能被修改,但如果初始化的是一个对象,那么不能修改的是该对象的内存地址;
const person = {
name: ‘布兰’
}
person.name = ‘bubuzou’
console.log(person.name) // ‘bubuzou’
person = ‘’ // TypeError
const/let
在全局作用域中声明的常量/变量不会挂到顶层对象(浏览器中是 window )的属性中;
var name = ‘布兰’
let age = 12
console.log(window.name) // ‘布兰’
console.log(window.age) // undefined
解构赋值
解构类型:
- 字符串解构
let [a, b, c = ‘c’] = ‘12’
console.log(a, b, c) // ‘1’ ‘2’ ‘c’
- 数值解构
let {toFixed: tf} = 10
console.log( tf.call(Math.PI, 2) ) // 3.14
- 布尔值解构
let {toString: ts} = true
console.log( ts.call(false) ) // ‘false’
- 数组解构:等号右侧的数据具有
Iterator
接口可以进行数组形式的解构赋值;
// 解构不成功的变量值为 undefined
let [a, b, c] = [1, 2]
console.log(a, b, c) // 1, 2, undefined
// 可以设置默认值
let [x, y, z = 3] = [1, 2, null]
console.log(x, y, z) // 1, 2, null
❝
什么样的数据具有
Iterator
接口呢?如果一个对象能够通过 [Symbol.iterator] 访问,且能够返回一个符合迭代器协议的对象,那么该对象就是可迭代的。目前内置的可迭代对象有:String、Array、TypeArray、Map、Set、arguments 和 NodeList 等。
❞
- 对象解构:与数组按照索引位置进行解构不同,对象解构是按照属性名进行解构赋值,如果在当前对象属性匹配不成功则会去对象的原型属性上查找:
// 默认写法
let { name: name, age: age } = { name: ‘布兰’, age: 12 }
// 简写
let { name, age } = { name: ‘布兰’, age: 12 }
// 改名且设置默认值
let { name: name1, age: age1 = 12 } = { name: ‘布兰’ }
console.log(name1, age1) // ‘布兰’ 12
- 函数参数解构:其实就是运用上面的对象解构和数组解构规则;
function move({x = 0, y = 0} = {}) {
console.log([x, y])
return [x, y];
}
move({x: 3, y: 8}) // [3, 8]
move({x: 3}) // [3, 0]
move({}) // [0, 0]
move() // [0, 0]
解构要点:
-
只要等号两边的模式相同(同是对象或同是数组),则左边的变量会被赋予对应的值;
-
解构不成功的变量值为
undefined
; -
默认值生效的前提是当等号右边对应的值全等于 undefined 的时候;
-
只要等号右边的值不是对象或者数组,则会进行自动装箱将其转成对象;
-
null
和undefined
都无法转成对象,所以无法解构。
解构应用:
- 交换变量的值;
let x = 1, y = 2;
[x, y] = [y, x]
console.log(x, y) // 2 1
- 通过函数返回对象属性
function getParams() {
return {
name: ‘布兰’,
age: 12,
}
}
let {name, age} = getParams()
- 通过定义函数参数来声明变量
let person = {
name: ‘布兰’,
age: 12
}
init(person)
// 普通用法
function init(person) {
let {name, age} = person
}
// 更简洁用法
function init({name, age}) {}
- 指定函数参数默认值
function initPerson({name = ‘布兰’, age = 12} = {}) {
console.log(name, age)
}
initPerson() // ‘布兰’ 12
initPerson({age: 20}) // ‘布兰’ 20
- 提取
JSON
数据
let responseData = {
code: 1000,
data: {},
message: ‘success’
}
let { code, data = {} } = responseData
- 遍历 Map 结构
let map = new Map()
map.set(‘beijing’, ‘北京’)
map.set(‘xiamen’, ‘厦门’)
for (let [key, value] of map) {
console.log(key, value)
}
- 输入模块的指定方法和属性
const { readFile, writeFile } = require(“fs”)
字符串扩展
- 可以使用
Unicode
编码来表示一个字符:
// 以下写法都可以用来表示字符 z
‘\z’ // 转义
‘\172’ // 十进制表示法
‘\x7A’ // 十六进制表示法
‘\u007A’ // Unicode 普通表示法
‘\u{7A}’ // Unicode 大括号表示法
❝
www.52unicode.com 这个网站可以查询到常见符号的 Unicode 编码。
❞
- 可以使用
for...of
正确遍历字符串:
let str = ‘???’
for (const emoji of str) {
console.log(emoji) // ???
}
for(let i = 0, l = str.length; i < l; i++) {
console.log(str[i]) // 不能正确输出表情
}
- 模板字符串使用两个反引号标识(``),可以用来定义多行字符串,或者使用它在字符串中插入变量:
let name = ‘hero’
let tips = `Hello ${name},
welcome to my world.`
alert( tips )
- 标签模板:在函数名后面接一个模板字符串相当于给函数传入了参数进行调用:
let name = ‘布兰’, age = 12;
let tips = parseHello ${name}, are you ${age} years old this year?
function parse(stringArr, …variable) {
}
// 相当于传递如下参数进行调用 parse 函数
parse(["Hello ", ", are you “, " years old this year?”], name, age)
String.fromCodePoint()
用于从Unicode
码点返回对应字符,可以支持0xFFFF
的码点:
String.fromCharCode(0x1f600) // “”
String.fromCodePoint(0x1f600) // “???”
String.raw()
返回把字符串所有变量替换且对斜杠进行转义的结果:
String.rawHi\n${2+3}!
// “Hi\n5!”
codePointAt()
返回字符的十进制码点,对于Unicode
大于0xFFFF
的字符,会被认为是2个字符,十进制码点转成十六进制可以使用toString(16)
:
let emoji = ‘???’
emoji.length // 2
emoji.charCodeAt(0).toString(16) // ‘d83d’
emoji.charCodeAt(1).toString(16) // ‘de00’
String.fromCodePoint(0xd83d, 0xde00) === ‘???’ // true
normalize()
方法会按照指定的一种Unicode
正规形式将当前字符串正规化。(如果该值不是字符串,则首先将其转换为一个字符串):
let str1 = ‘\u00F1’
let str2 = ‘\u006E\u0303’
str1 // ñ
str2 // ñ
str1 === str2 // false
str1.length === str2.length // false
str1.normalize() === str2.normalize() // true
- 字符串是否包含子串:
let s = ‘Hello world!’
s.includes(‘o’) // true
s.startsWith(‘Hello’) // true
s.endsWith(‘!’) // true
这三个方法都支持第二个参数,表示开始搜索的位置:
let s = ‘Hello world!’
s.includes(‘Hello’, 6) // false
s.startsWith(‘world’, 6) // true
s.endsWith(‘Hello’, 5) // true
上面代码表示,使用第二个参数 n
时,endsWith
的行为与其他两个方法有所不同。它针对前 n
个字符,而其他两个方法针对从第 n
个位置直到字符串结束。
repeat(n)
将当前字符串重复n
次后,返回一个新字符串:
‘x’.repeat(2) // ‘xx’
‘x’.repeat(1.9) // ‘x’
‘x’.repeat(NaN) // ‘’
‘x’.repeat(undefined) // ‘’
‘x’.repeat(‘2a’) // ‘’
‘x’.repeat(-0.6) // ‘’,解释:0 ~ 1 之间的小数相当于 0
‘x’.repeat(-2) // RangeError
‘x’.repeat(Infinity) // RangeError
数值扩展
- 二进制(0b)和八进制(0o)表示法:
let num = 100
let b = num.toString(2) // 二进制的100:1100100
let o = num.toString(8) // 八进制的100:144
0b1100100 === 100 // true
0o144 === 100 // true
Number.isFinite()
判断一个数是否是有限的数,入参如果不是数值一律返回false
:
Number.isFinite(-2.9) // true
Number.isFinite(NaN) // false
Number.isFinite(‘’) // false
Number.isFinite(false) // true
Number.isFinite(Infinity) // false
Number.isNaN()
判断一个数值是否为NaN
,如果入参不是NaN
那结果都是false
:
Number.isNaN(NaN) // true
Number.isFinite(‘a’/0) // true
Number.isFinite(‘NaN’) // false
- 数值转化:
Number.parseInt()
和Number.parseFloat()
,非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:
parseInt(‘12a’) // 12
parseInt(‘a12’) // NaN
parseInt(‘’) // NaN
parseInt(‘0xA’) // 10,0x开头的将会被当成十六进制数
parseInt()
默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:
parseInt(‘1010’, 2) // 10
parseInt(‘ff’, 16) // 255
参考:parseInt
Number.isInteger()
判断一个数值是否为整数,入参为非数值则一定返回false
:
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger(‘15’) // false
Number.isInteger(true) // false
Number.isInteger(3.0000000000000002) // true
❝
如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。
❞
Number.EPSILON
表示一个可接受的最小误差范围,通常用于浮点数运算:
0.1 + 0.2 === 0.3 // false
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
Number.isSafeInteger()
用来判断一个数是否在最大安全整数(Number.MAX_SAFE_INTEGER
)和最小安全整数(Number.MIN_SAFE_INTEGER
)之间:
Number.MAX_SAFE_INTEGER === 2 ** 53 -1 // true
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true
Number.isSafeInteger(2) // true
Number.isSafeInteger(‘2’) // false
Number.isSafeInteger(Infinity) // false
-
Math.trunc()
:返回数值整数部分 -
Math.sign()
:返回数值类型(正数 1、负数 -1、零 0) -
Math.cbrt()
:返回数值立方根 -
Math.clz32()
:返回数值的 32 位无符号整数形式 -
Math.imul()
:返回两个数值相乘 -
Math.fround()
:返回数值的 32 位单精度浮点数形式 -
Math.hypot()
:返回所有数值平方和的平方根 -
Math.expm1()
:返回e^n - 1
-
Math.log1p()
:返回 1 + n 的自然对数(Math.log(1 + n)
) -
Math.log10()
:返回以 10 为底的 n 的对数 -
Math.log2()
:返回以 2 为底的n的对数 -
Math.sinh()
:返回 n 的双曲正弦 -
Math.cosh()
:返回 n 的双曲余弦 -
Math.tanh()
:返回 n 的双曲正切 -
Math.asinh()
:返回 n 的反双曲正弦 -
Math.acosh()
:返回 n 的反双曲余弦 -
Math.atanh()
:返回 n 的反双曲正切
数组扩展
- 数组扩展运算符(…)将数组展开成用逗号分隔的参数序列,只能展开一层数组:
// 应用一:函数传参
Math.max(…[1, 2, 3]) // 3
// 应用二:数组合并
let merge = […[1, 2], …[3, 4], 5, 6] // 1, 2, 3, 4, 5, 6
// 应用三:浅克隆
let a = [1, 2, 3]
let clone = […a]
a === clone // false
// 应用四:数组解构
const [x, …y] = [1, 2, 3]
x // 1
y // [2, 3]
Array.from()
可以将类数组对象(NodeList
,arguments
)和可迭代对象转成数组:
// 应用一:字符串转数组
Array.from(‘foo’) // [‘f’, ‘o’, ‘o’]
// 应用二:数组合并去重
let merge = […[1, 2], …[2, 3]]
Array.from(new Set(merge)) // [‘1’, ‘2’, ‘3’]
// 应用三:arguments 转数组
function f() {
return Array.from(arguments)
}
f(1, 2, 3) // [1, 2, 3]
如果 Array.from()
带第二个参数 mapFn
,将对生成的新数组执行一次 map
操作:
Array.from([1, 2, 3], (x) => x * x ) // [1, 4, 9]
Array.from({length: 3}, (v, i) => ++i) // [1, 2, 3]
Array.of()
将一组参数转成数组:
Array.of(1, 2, 3) // [1, 2, 3]
// 类似于
function arrayOf(…params){
return [].slice.call(params)
}
arrayOf(1, 2, 3) // [1, 2, 3]
Array.copyWithin()
在当前数组内部,将制定位置的成员复制到其他位置(会覆盖原来位置的成员),最后返回一个新数组。接收 3 个参数,参数为负数表示右边开始计算:
[1, 2, 3, 4, 5].copyWithin(-1) // [1, 2, 3, 4, 1]
[1, 2, 3, 4, 5].copyWithin(1) // [1, 1, 2, 3, 4]
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5]
[1, 2, 3, 4, 5].copyWithin(0, -3, -1) // [3, 4, 3, 4, 5]
- 查找第一个出现的子成员:
find()
和findIndex()
:
// 找出第一个偶数
[1, 6, 9].find((val, index, arr) => val % 2 === 0) // 6
// 找出第一个偶数的索引位置
[1, 6, 9].findIndex((val, index, arr) => val % 2 === 0) // 1
fill()
使用给定的值来填充数组,有 3 个参数:
// 初始化空数组
Array(3).fill(1) // [1, 1, 1]
[1, 2, 3, 4].fill(‘a’, 2, 4) // [1, 2, ‘a’, ‘a’]
- 通过
keys()
(键名)、entries()
(键值)和values()
(键值对) 获取数组迭代器对象,可以被for...of
迭代,
let arr = [‘a’, ‘b’, ‘c’]
for (let x of arr.keys()) {
console.log(x) // 1, 2, 3
}
for (let v of arr.values()) {
console.log(v) // ‘a’ ‘b’ ‘c’
}
for (let e of arr.entries()) {
console.log(e) // [0, ‘a’] [0, ‘b’] [0, ‘c’]
}
- 数组空位,是指数组没有值,比如:
[,,]
,而像这种[undefined]
是不包含空位的。由于ES6
之前的一些API
对空位的处理规则很不一致,所以实际操作的时候应该尽量避免空位的出现,而为了改变这个现状,ES6
的API
会默认将空位处理成undefined
:
[…[1, , 3].values()] // [1, undefined, 3]
[1, , 3].findIndex(x => x === undefined) // 1
对象扩展
- 对象属性简写:
let name = ‘布兰’
let person = {
name,
getName() {
return this.name
}
}
// 等同于
let person1 = {
name: ‘布兰’,
getName: function() {
return this.name
}
}
- 属性名表达式:在用对象字面量定义对象的时候,允许通过属性名表达式来定义对象属性:
let name = ‘name’,
let person = {
return this.name
}
}
- 方法的
name
属性,存在好几种情况,这里仅列出常见的几种:
情况一:普通对象方法的 name
属性直接返回方法名,函数声明亦是如此,函数表达式返回变量名:
let person = {
hi(){}
}
person.hi.name // ‘hi’
情况二:构造函数的 name
为 anonymous
:
(new Function).name // ‘anonymous’
情况三:绑定函数的 name
将会在函数名前加上 bound
:
function foo() {}
foo.bind({}).name // ‘bound foo’
情况四:如果对象的方法使用了取值函数(getter
)和存值函数(setter
),则 name
属性不是在该方法上面,而是该方法的属性的描述对象的 get
和 set
属性上面:
let o = {
get foo(){},
set foo(x){}
}
o.foo.name // TypeError: Cannot read property ‘name’ of undefined
let descriptor = Object.getOwnPropertyDescriptor(o, “foo”)
descriptor.get.name // “get foo”
descriptor.set.name // “set foo”
参考:function_name
- 属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor
),用来控制该属性的行为。可以通过 Object.getOwnPropertyDescriptor()
来获取对象某个属性的描述:
let person = { name: ‘布兰’, age: 12 }
Object.getOwnPropertyDescriptor(person, ‘name’)
// {
// configurable: true,
// enumerable: true,
// value: “布兰”,
// writable: true,
// }
这里的 enumerable
就是对象某个属性的可枚举属性,如果某个属性的 enumerable
值为 false
则表示该属性不能被枚举,所以该属性会被如下 4 种操作忽略:
let person = { name: ‘布兰’ }
Object.defineProperty(person, ‘age’, {
configurable: true,
enumerable: false,
value: 12,
writable: true
})
person // { name: ‘布兰’, age: 12 }
// 以下操作都将忽略 person 对象的 age 属性
for (let x in person) {
console.log(x) // ‘name’
}
Object.keys(person) // [‘name’]
JSON.stringify(person) // ‘{“name”: “布兰”}’
Object.assign({}, person) // { name: ‘布兰’ }
Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol
或字符串,也不管是否可枚举:
// 基于上面的代码
Reflect.ownKeys(person) // [‘name’, ‘age’]
super
关键字,指向对象的原型对象,只能用于对象的方法中,其他地方将报错:
let person = {
name: ‘布兰’,
getName() {
return super.name
}
}
Object.setPrototypeOf(person, {name: ‘hello’})
person.getName() // ‘hello’
// 以下几种 super 的使用将报错
const obj1 = {
foo: super.foo
}
const obj2 = {
foo: () => super.foo
}
const obj3 = {
foo: function () {
return super.foo
}
}
Object.is()
用来判断两个值是否相等,表现基本和===
一样,除了以下两种情况:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
用于对象的合并,将源对象(source
)的所有可枚举属性,复制到目标对象(target
),如果有同名属性,则后面的会直接替换前面的:
let target = { a: 1 }
let source1 = { a: 2, b: 3, d: {e: 1, f: 2} }
let source2 = { a: 3, c: 4, d: {g: 3} }
Object.assign(target, source1, source2)
target // { a: 3, b: 3, c: 4, d: {g: 3} }
Object.assign()
实行的是浅拷贝,如果源对象某个属性是对象,那么拷贝的是这个对象的引用:
let target = {a: {b: 1}}
let source = {a: {b: 2}}
Object.assign(target, source)
target.a.b = 3
source.a.b // 3
-
__proto__
属性是用来读取和设置当前对象的原型,而由于其下划线更多的是表面其是一个内部属性,所以建议不在正式场合使用它,而是用下面的Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)代替。 -
Object.setPrototypeOf()
用于设置对象原型,Object.getPrototypeOf()
用于读取对象原型:
let person = {name: ‘布兰’}
Object.setPrototypeOf(person, {name: ‘动物’})
Object.getPrototypeOf(person) // {name: ‘动物’}
正则扩展
RegExp
构造函数,允许首参为正则表达式,第二个参数为修饰符,如果有第二个参数,则修饰符以第二个为准:
let reg = new RegExp(/xYz\d+/gi, i)
reg.flags // ‘i’
-
正则方法调用变更:字符串对象的
match()
、replace()
、search()
、split()
内部调用转为调用RegExp
实例对应的RegExp.prototype[Symbol.方法]
; -
u
修饰符:含义为Unicode
模式,用来正确处理大于\uFFFF
的Unicode
字符。也就是说,如果待匹配的字符串中可能包含有大于\uFFFF
的字符,就必须加上u
修饰符,才能正确处理。
// 加上 u 修饰符才能让 . 字符正确识别大于 \uFFFF 的字符
/^.$/.test(‘???’) // false
/^.$/u.test(‘???’) // true
// 大括号 Unicode 字符表示法必须加上 u 修饰符
/\u{61}/.test(‘a’) // false
/\u{61}/u.test(‘a’) // true
// 有 u 修饰符,量词才能正确匹配大于 \uFFFF 的字符
/???{2}/.test(‘???’) // false
/???{2}/u.test(‘???’) // true
RegExp.prototype.unicode
属性表示正则是否设置了 u
修饰符:
/???/.unicode // false
/???/u.unicode // true
y
修饰符,与g
修饰符类似也是全局匹配;不同的是g
是剩余字符中匹配即可,而y
则是必须在剩余的第一个字符开始匹配才行,所以y
修饰符也叫黏连修饰符:
let s = ‘aaa_aa_a’
let r1 = /a+/g
let r2 = /a+/y
r1.exec(s) // [“aaa”]
r2.exec(s) // [“aaa”]
r1.exec(s) // [“aa”]
r2.exec(s) // null
RegExp.prototype.sticky
属性表示是否设置了 y
修饰符:
/abc/y.sticky // true
RegExp.prototype.flags
属性会返回当前正则的所有修饰符:
/abc???/iuy.flags // ‘iuy’
函数扩展
- 函数参数默认值。参数不能有同名的,函数体内不能用
let
和const
声明同参数名的变量:
function printInfo(name = ‘布兰’, age = 12) {}
使用参数默认值的时候,参数不能有同名的:
function f(x, x, y) {} // 不报错
function f(x, x, y = 1) {} // 报错
函数体内不能用 let
和 const
声明同参数名的变量:
// 报错
function f(x, y) {
let x = 0
}
函数的 length
属性会返回没有指定默认值的参数个数,且如果设置默认值的参数不是尾参数,则 length
不再计入后面的参数:
(function f(x, y){}).length // 2
(function f(x, y = 1){}).length // 1
(function f(x = 1, y){}).length // 0
- 剩余(
rest
) 参数(…变量名)的形式,用于获取函数的剩余参数,注意rest
参数必须放在最后一个位置,可以很好的代替arguments
对象:
function f(x, …y) {
console.log(x) // 1
for (let val of y) {
coonsole.log(val) // 2 3
}
}
f(1, 2, 3)
- 严格模式:只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数体内就不能显示的设定为严格模式,因为严格模式的作用范围包含了函数参数,而函数执行的顺序是先执行参数,然后再执行函数体,执行到函数体里的
use strict
的时候,那么此时因为函数参数已经执行完成了,那函数参数还要不要受到严格模式的限制呢?这就出现矛盾了。规避限制的办法有两个:设置全局的严格模式或者在函数体外在包一个立即执行函数并且声明严格模式:
// 解法一
‘use strict’
function f(x, y = 2) {
}
// 解法二
let f = (function(){
‘use strict’
return function(x, y = 2) {}
})()
- 箭头函数语法比函数表达式更简洁,并且没有自己的
this
、arguments
,不能用作构造函数和用作生成器。
几种箭头函数写法:
let f1 = () => {} // 没有参数
let f2 = (x) => {} // 1个参数
let f3 = x => {} // 1个参数可以省略圆括号
let f4 = (x, y) => {} // 2个参数以上必须加上圆括号
let f5 = (x = 1, y = 2) => {} // 支持参数默认值
let f6 = (x, …y) => {} // 支持 rest 参数
let f7 = ({x = 1, y = 2} = {}) // 支持参数解构
箭头函数没有自己的 this
:
function Person(){
this.age = 0
setInterval(() => {
this.age++
}, 1000)
}
var p = new Person() // 1 秒后 Person {age: 1}
通过 call/apply
调用箭头函数的时候将不会绑定第一个参数的作用域:
let adder = {
base: 1,
add: function(a) {
let f = v => v + this.base
return f(a)
},
addThruCall: function(a) {
let f = v => v + this.base
let b = {
base: 2
}
return f.call(b, a)
}
}
adder.add(1) // 输出 2
adder.addThruCall(1) // 仍然输出 2
箭头函数没有自己的 arguments
对象,不过可以使用 rest
参数代替:
let log = () => {
console.log(arguments) // ReferenceError
}
log(2, 3)
// 剩余参数代替写法
let restLog = (…arr) => {
console.log(arr) // [2, 3]
}
restLog(2, 3)
箭头函数不能用作构造器,和 new
一起用会抛出错误:
let Foo = () => {}
let foo = new Foo()
// TypeError: Foo is not a constructor
箭头函数返回对象字面量,需要用圆括号包起来:
let func2 = () => ({foo: 1})
参考:Arrow_functions
- 尾调用和尾递归
首先得知道什么是尾调用:函数的最后一步调用另外一个函数:
// 是尾调用
function f(x) {
return g(x)
}
// 以下都不是尾调用
function f(x) {
let y = g(x)
return y
}
function f(x) {
let y = g(x)
return g(x) + 1
}
function f(x) {
g(x)
// 因为最后一步是 return: undefined
}
尾调用有啥用?我们知道函数的相互调用是会生成“调用帧”的,而“调用帧”里存了各种信息比如函数的内部变量和调用函数的位置等,所有的“调用帧”组成了一个“调用栈”。如果在函数的最后一步操作调用了另外一个函数,因为外层函数里调用位置、内部变量等信息都不会再用到了,所有就无需保留外层函数的“调用帧”了,只要直接用内层函数的“调用帧”取代外层函数的“调用帧”即可:
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 等同于
function f() {
return g(3)
}
f()
// 等同于
g(3)
这样一来就很明显的减少了调用栈中的帧数,内存占用就少了,所以这就是尾调用的优化作用。尾递归也是如此,递归如果次数多那就需要保留非常多的“调用帧”,所以经常会出现栈溢出错误,而使用了尾递归优化后就不会发生栈溢出的错误了:
// 常规递归的斐波那契函数
function Fibonacci(n) {
if ( n <= 1 ) {return 1}
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(100) // 超时
// 尾递归优化后的斐波那契函数
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
if( n <= 1 ) {return ac2}
return Fibonacci2(n - 1, ac2, ac1 + ac2)
}s
Fibonacci2(100) // 573147844013817200000
Symbol
Symbol
是一个新的原始类型,用来表示一个独一无二的值,可以通过Symbol()
函数来创建一个Symbol
类型的值,为了加以区分,可以传入一个字符串作为其描述:
let s1 = Symbol(‘foo’)
let s2 = Symbol(‘foo’)
s1 === s2 // false
Symbol
类型无法通过数学运算符进行隐式类型转换,但是可以通过String()
显示转成字符串或者通过Boolean()
显示转成布尔值:
let s = Symbol(‘foo’)
String(s) // “Symbol(‘foo’)”
s.toString() // “Symbol(‘foo’)”
Boolean(s) // true
- 引入
Symbol
最大的初衷其实就是为了让它作为对象的属性名而使用,这样就可以有效避免属性名的冲突了:
let foo = Symbol(‘foo’)
let obj = {
foo: ‘foo2’
}
objfoo // ‘foo1’
obj.foo // ‘foo2’
Symbol
属性的不可枚举性,不会被for...in
、for...of
、Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
等枚举:
let person = {
name: ‘布兰’,
}
for (let x in person) {
console.log(x) // ‘name’
}
Object.keys(person) // [‘name’]
Object.getOwnPropertyNames(person) // [‘name’]
JSON.stringify(person) // ‘{“name”:“布兰”}’
但是可以通过 Object.getOwnPropertySymbols()
获取到对象的所有 Symbol
属性名,返回一个数组:
// 基于上面的代码
Object.getOwnPropertySymbols(person) // [Symbol(age)]
静态方法:
Symbol.for()
按照描述去全局查找Symbol
,找不到则在全局登记一个:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
基础面试题
主要内容包括:HTML,CSS,JavaScript,浏览器,性能优化等等
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
bonacci2(100) // 573147844013817200000
Symbol
Symbol
是一个新的原始类型,用来表示一个独一无二的值,可以通过Symbol()
函数来创建一个Symbol
类型的值,为了加以区分,可以传入一个字符串作为其描述:
let s1 = Symbol(‘foo’)
let s2 = Symbol(‘foo’)
s1 === s2 // false
Symbol
类型无法通过数学运算符进行隐式类型转换,但是可以通过String()
显示转成字符串或者通过Boolean()
显示转成布尔值:
let s = Symbol(‘foo’)
String(s) // “Symbol(‘foo’)”
s.toString() // “Symbol(‘foo’)”
Boolean(s) // true
- 引入
Symbol
最大的初衷其实就是为了让它作为对象的属性名而使用,这样就可以有效避免属性名的冲突了:
let foo = Symbol(‘foo’)
let obj = {
foo: ‘foo2’
}
objfoo // ‘foo1’
obj.foo // ‘foo2’
Symbol
属性的不可枚举性,不会被for...in
、for...of
、Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
等枚举:
let person = {
name: ‘布兰’,
}
for (let x in person) {
console.log(x) // ‘name’
}
Object.keys(person) // [‘name’]
Object.getOwnPropertyNames(person) // [‘name’]
JSON.stringify(person) // ‘{“name”:“布兰”}’
但是可以通过 Object.getOwnPropertySymbols()
获取到对象的所有 Symbol
属性名,返回一个数组:
// 基于上面的代码
Object.getOwnPropertySymbols(person) // [Symbol(age)]
静态方法:
Symbol.for()
按照描述去全局查找Symbol
,找不到则在全局登记一个:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-2hLCtH0j-1712735067292)]
[外链图片转存中…(img-2o0KfAME-1712735067293)]
[外链图片转存中…(img-QivwFtvo-1712735067294)]
[外链图片转存中…(img-aCtZQicu-1712735067294)]
[外链图片转存中…(img-TXB0Wtr1-1712735067294)]
[外链图片转存中…(img-BICCJWvl-1712735067295)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-N1xK8mCX-1712735067295)]
基础面试题
主要内容包括:HTML,CSS,JavaScript,浏览器,性能优化等等
[外链图片转存中…(img-PNOvGACD-1712735067295)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-gDPUrqQe-1712735067295)]