文章根据codewhy老师的课程学习整理深入JavaScript高级语法-coderwhy大神新课-学习视频教程-腾讯课堂
一、认识class定义类
我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。
在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类;
但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;
可以使用两种方式来声明类:类声明和类表达式
// 类的声明
class Person {
}
// 类的表达式
var Animal = class {
}
类和构造函数的异同
类的一些特性和我们的构造函数的特性其实是一致的
// 研究一下类的特性
console.log(Person) //[class Person]
console.log(Person.prototype) //{}
console.log(Person.prototype.__proto__) //[Object: null prototype] {}
console.log(Person.prototype.constructor) //[class Person]
console.log(typeof Person) // function
var p = new Person()
console.log(p.__proto__ === Person.prototype) // true
1. 类的构造函数
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
????????每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
????????当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
????????每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:
????????1.在内存中创建一个新的对象(空对象);
????????2.这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
????????3.构造函数内部的this,会指向创建出来的新对象;
????????4.执行构造函数的内部代码(函数体代码);
????????5.如果构造函数没有返回非空对象,则返回创建出来的新对象;
2. 类的实例方法
在上面我们定义的属性都是直接放到了this上,也就意味着它是放到了创建出来的新对象中:
????????在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;
????????这个时候我们可以直接在类中定义
// 类的声明
class Person {
// 类的构造方法
// 注意: 一个类只能有一个构造函数
// 1.在内存中创建一个对象 moni = {}
// 2.将类的原型prototype赋值给创建出来的对象 moni.__proto__ = Person.prototype
// 3.将对象赋值给函数的this: new绑定 this = moni
// 4.执行函数体中的代码
// 5.自动返回创建出来的对象
constructor(name, age) {
this.name = name
this.age = age
}
eating() {
console.log(this.name + " eating~")
}
running() {
console.log(this.name + " running~")
}
}
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
p.eating()
p.running()
console.log(p1, p2)
3. 类的访问器方法
我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的:
4. 类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
var names = ["abc", "cba", "nba"]
class Person {
constructor(name, age) {
this.name = name
this.age = age
this._address = "广州市"
}
// 普通的实例方法
// 创建出来的对象进行访问
// var p = new Person()
// p.eating()
eating() {
console.log(this.name + " eating~")
}
running() {
console.log(this.name + " running~")
}
// 类的访问器方法
get address() {
console.log("拦截访问操作")
return this._address
}
set address(newAddress) {
console.log("拦截设置操作")
this._address = newAddress
}
// 类的静态方法(类方法)
// Person.createPerson()
static randomPerson() {
var nameIndex = Math.floor(Math.random() * names.length)
var name = names[nameIndex]
var age = Math.floor(Math.random() * 100)
return new Person(name, age)
}
}
var p = new Person("why", 18)
p.eating()
p.running()
console.log(p.address)
p.address = "北京市"
console.log(p.address)
// console.log(Object.getOwnPropertyDescriptors(Person.prototype))
for (var i = 0; i < 50; i++) {
console.log(Person.randomPerson())
}
二、ES6
1. 类的继承 - extends
在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承:
super关键字
在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!
super的使用位置有三个:子类的构造函数、实例方法、静态方法
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
running() {
console.log(this.name + " running~")
}
eating() {
console.log(this.name + " eating~")
}
personMethod() {
console.log("处理逻辑1")
console.log("处理逻辑2")
console.log("处理逻辑3")
}
static staticMethod() {
console.log("PersonStaticMethod")
}
}
// Student称之为子类(派生类)
class Student extends Person {
// JS引擎在解析子类的时候就有要求, 如果我们有实现继承
// 那么子类的构造方法中, 在使用this之前
constructor(name, age, sno) {
super(name, age)
this.sno = sno
}
studying() {
console.log(this.name + " studying~")
}
// 类对父类的方法的重写
running() {
console.log("student " + this.name + " running")
}
// 重写personMethod方法
personMethod() {
// 复用父类中的处理逻辑
super.personMethod()
console.log("处理逻辑4")
console.log("处理逻辑5")
console.log("处理逻辑6")
}
// 重写静态方法
static staticMethod() {
super.staticMethod()
console.log("StudentStaticMethod")
}
}
var stu = new Student("why", 18, 111)
console.log(stu)
继承内置类
我们也可以让我们的类继承自内置类,比如Array
class HYArray extends Array {
firstItem() {
return this[0]
}
lastItem() {
return this[this.length-1]
}
}
var arr = new HYArray(1, 2, 3)
console.log(arr.firstItem())
console.log(arr.lastItem())
类的混入mixin
JavaScript的类只支持单继承:也就是只能有一个父类
????????那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢?
????????这个时候我们可以使用混入(mixin);
class Person {
}
function mixinRunner(BaseClass) {
class NewClass extends BaseClass {
running() {
console.log("running~")
}
}
return NewClass
}
function mixinEater(BaseClass) {
return class extends BaseClass {
eating() {
console.log("eating~")
}
}
}
// 在JS中类只能有一个父类: 单继承
class Student extends Person {
}
var NewStudent = mixinEater(mixinRunner(Student))
var ns = new NewStudent()
ns.running()
ns.eating()
2. JavaScript中的多态
不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现
那么从上面的定义来看,JavaScript是一定存在多态的
// 传统的面向对象多态是有三个前提:
// 1> 必须有继承(是多态的前提)
// 2> 必须有重写(子类重写父类的方法)
// 3> 必须有父类引用指向子类对象
// Shape形状
class Shape {
getArea() {}
}
class Rectangle extends Shape {
getArea() {
return 100
}
}
class Circle extends Shape {
getArea() {
return 200
}
}
var r = new Rectangle()
var c = new Circle()
// 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现.
function calcArea(shape: Shape) {
console.log(shape.getArea())
}
calcArea(r)
calcArea(c)
3.字面量的增强
ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)
字面量的增强主要包括下面几部分:
属性的简写:Property Shorthand
方法的简写:Method Shorthand
计算属性名:Computed Property Names
var name = "why"
var age = 18
var obj = {
// 1.property shorthand(属性的简写)
name,
age,
// 2.method shorthand(方法的简写)
foo: function() {
console.log(this)
},
bar() {
console.log(this)
},
baz: () => {
console.log(this)
},
// 3.computed property name(计算属性名)
[name + 123]: 'hehehehe'
}
obj.baz()
obj.bar()
obj.foo()
// obj[name + 123] = "hahaha"
console.log(obj)
// {
// name: 'why',
// age: 18,
// foo: [Function: foo],
// bar: [Function: bar],
// baz: [Function: baz],
// why123: 'hehehehe'
// }
4. 解构Destructuring
ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring。 n
我们可以划分为:数组的解构和对象的解构
数组的解构:?
对象的解构:
var names = ["abc", "cba", "nba"]
// 对数组的解构: []
var [item1, item2, item3] = names
console.log(item1, item2, item3) //abc cba nba
// 解构后面的元素
var [, , itemz] = names
console.log(itemz) //nba
// 解构出一个元素,后面的元素放到一个新数组中
var [itemx, ...newNames] = names
console.log(itemx, newNames) //abc [ 'cba', 'nba' ]
// 解构的默认值
var [itema, itemb, itemc, itemd = "aaa"] = names
console.log(itemd) //aaa
var obj = {
name: "why",
age: 18,
height: 1.88
}
// 对象的解构: {}
var { name, age, height } = obj
console.log(name, age, height) //why 18 1.88
var { age } = obj
console.log(age) //18
var { name: newName } = obj
console.log(newName) //why
var { address: newAddress = "广州市" } = obj
console.log(newAddress) //广州市
function foo(info) {
console.log(info.name, info.age)
}
foo(obj) //why 18
function bar({name, age}) {
console.log(name, age)
}
bar(obj) //why 18
解构目前在开发中使用是非常多的:
- 比如在开发中拿到一个变量时,自动对其进行解构使用;
- 比如对函数的参数进行解构; 解构的应用场景
5.?let/const基本使用
在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
注意事项一: const本质上是传递的值不可以修改
但是如果传递的是一个引用类型(内存地址), 可以通过引用找到对应的对象, 去修改对象内部的属性, 这个是可以的
let/const和var的区别
1. 通过let/const定义的变量名是不可以重复定义
2.??let、const没有进行作用域提升,但是会在解析阶段被创建出来
3. Window对象添加属性
- 全局通过var来声明一个变量,事实上会在window上添加一个属性:
- 但是let、const是不会给window上添加任何属性的。
- 变量被保存到VariableMap中
也就是说我们声明的变量和环境记录是被添加到变量环境中的
- v8中其实是通过VariableMap的一个hashmap来实现它们的存储的。
- 那么window对象呢?而window对象是早期的GO对象,在最新的实现中其实是浏览器添加的全局对象,并且 一直保持了window和var之间值的相等性
4.块级作用域区别
ES5中没有块级作用域,只有两个东西会形成作用域
在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的
但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的: p这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升
var、let、const的选择
var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些 历史遗留问题
在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;
对于let、const:
- 我们会有限推荐使用const,这样可以保证数据的安全性不会被随意的篡改;
- ?只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let
6.字符串模板基本使用
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
- ?首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
- ?其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容
// ES6之前拼接字符串和其他标识符
const name = "why"
const age = 18
const height = 1.88
// console.log("my name is " + name + ", age is " + age + ", height is " + height)
// ES6提供模板字符串 ``
const message = `my name is ${name}, age is ${age}, height is ${height}`
console.log(message) //my name is why, age is 18, height is 1.88
const info = `age double is ${age * 2}`
console.log(info) //age double is 36
function doubleAge() {
return age * 2
}
const info2 = `double age is ${doubleAge()}`
console.log(info2) //double age is 36
标签模板字符串使用
如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
- 模板字符串被拆分了;?
- 第一个元素是数组,是被模块字符串拆分的字符串组合;
- 后面的元素是一个个模块字符串传入的内容
// 第一个参数依然是模块字符串中整个字符串, 只是被切成多块,放到了一个数组中
// 第二个参数是模块字符串中, 第一个 ${}
function foo(m, n, x) {
console.log(m, n, x, '---------')
}
// foo("Hello", "World")
// 另外调用函数的方式: 标签模块字符串
// foo``
// foo`Hello World`
const name = "why"
const age = 18
// ['Hello', 'Wo', 'rld']
foo`Hello${name}Wo${age}rld`
7.函数的默认参数
在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:
- 传入了参数,那么使用传入的参数;
- 没有传入参数,那么使用一个默认值;
而在ES6中,我们允许给函数一个默认值:
函数默认值的补充
默认值也可以和解构一起来使用?
另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的)
- 但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配;
另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了、
函数的剩余参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
- 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
那么剩余参数和arguments有什么区别呢?
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供 并且希望以此来替代arguments的;
剩余参数必须放到最后一个位置,否则会报错
// 1.ES6可以给函数参数提供默认值
function foo(m = "aaa", n = "bbb") {
console.log(m, n)
}
// foo()
foo(0, "") //0
// 2.对象参数和默认值以及解构
function printInfo({name, age} = {name: "why", age: 18}) {
console.log(name, age)
}
printInfo({name: "kobe", age: 40}) //kobe 40
// 另外一种写法
function printInfo1({name = "why", age = 18} = {}) {
console.log(name, age)
}
printInfo1()// why 18
// 3.有默认值的形参最好放到最后
function bar(x, y, z = 30) {
console.log(x, y, z)
}
bar(10, 20) //10 20 30
bar(undefined, 10, 20) //undefined 10 20
// 4.有默认值的函数的length属性
function baz(x, y, z, m, n = 30) {
console.log(x, y, z, m, n)
}
console.log(baz.length) //4
?8. ES6中展开语法的使用
const names = ["abc", "cba", "nba"]
const name = "why"
const info = {name: "why", age: 18}
// 1.函数调用时
function foo(x, y, z) {
console.log(x, y, z)
}
// foo.apply(null, names)
foo(...names) //abc cba nba
foo(...name) //w h y
// 2.构造数组时
const newNames = [...names, ...name]
console.log(newNames) //[ 'abc', 'cba', 'nba', 'w', 'h', 'y' ]
// 3.构建对象字面量时ES2018(ES9)
const obj = { ...info, address: "广州市", ...names }
console.log(obj)
// {
// '0': 'abc',
// '1': 'cba',
// '2': 'nba',
// name: 'why',
// age: 18,
// address: '广州市'
// }
9.ES6中的数值表示
const num1 = 100 // 十进制
// b -> binary
const num2 = 0b100 // 二进制
// o -> octonary
const num3 = 0o100 // 八进制
// x -> hexadecimal
const num4 = 0x100 // 十六进制
console.log(num1, num2, num3, num4)
// 大的数值的连接符(ES2021 ES12)
const num = 10_000_000_000_000_000
console.log(num)
10.Symbol的基本使用方式
// 1.ES6之前, 对象的属性名(key)
var obj1 = {
name: "why",
friend: { name: "kobe" },
age: 18
}
obj1['newName'] = "james"
console.log(obj1) //{ name: 'why', friend: { name: 'kobe' }, age: 18, newName: 'james' }
// 2.ES6中Symbol的基本使用:生成的值都是唯一的
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2) //false
// ES2019(ES10)中, Symbol还有一个描述(description)
const s3 = Symbol("aaa")
console.log(s3.description) //aaa
// 3.Symbol值作为key
// 3.1.在定义对象字面量时使用
const obj = {
[s1]: "abc",
[s2]: "cba"
}
// 3.2.新增属性
obj[s3] = "nba"
// 3.3.Object.defineProperty方式
const s4 = Symbol()
Object.defineProperty(obj, s4, {
enumerable: true,
configurable: true,
writable: true,
value: "mba"
})
console.log(obj[s1], obj[s2], obj[s3], obj[s4]) //abc cba nba mba
// 注意: 不能通过.语法获取
// console.log(obj.s1)
// 4.使用Symbol作为key的属性名,在遍历/Object.keys等中是获取不到这些Symbol值
// 需要Object.getOwnPropertySymbols来获取所有Symbol的key
console.log(Object.keys(obj)) //[]
console.log(Object.getOwnPropertyNames(obj)) //[]
console.log(Object.getOwnPropertySymbols(obj)) //[ Symbol(), Symbol(), Symbol(aaa), Symbol() ]
const sKeys = Object.getOwnPropertySymbols(obj)
for (const sKey of sKeys) {
console.log(obj[sKey]) //abc cba nba mba
}
// 5.Symbol.for(key)/Symbol.keyFor(symbol)
const sa = Symbol.for("aaa")
const sb = Symbol.for("aaa")
console.log(sa === sb) //true
console.log(sa) //Symbol(aaa)
const key = Symbol.keyFor(sa)
console.log(key) //aaa
const sc = Symbol.for(key)
console.log(sa === sc) //true
11.Set的基本使用
在ES6之前,我们存储数据的结构主要有两种:数组、对象
- 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap
Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。
- 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):?
我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重
Set常见的属性:
Set常用的方法:
- add(value):添加某个元素,返回Set对象本身;
- delete(value):从set中删除和这个值相等的元素,返回boolean类型;
- has(value):判断set中是否存在某个元素,返回boolean类型;
- clear():清空set中所有的元素,没有返回值;
- forEach(callback, [, thisArg]):通过forEach遍历set;
另外Set是支持for of的遍历的。
// 10, 20, 40, 333
// 1.创建Set结构
const set = new Set()
set.add(10)
set.add(20)
set.add(40)
set.add(333)
set.add(10)
// 2.添加对象时特别注意:
set.add({})
set.add({})
const obj = {}
set.add(obj)
set.add(obj)
// console.log(set)
// 3.对数组去重(去除重复的元素)
const arr = [33, 10, 26, 30, 33, 26]
// const newArr = []
// for (const item of arr) {
// if (newArr.indexOf(item) !== -1) {
// newArr.push(item)
// }
// }
const arrSet = new Set(arr)
const newArr = Array.from(arrSet)
// const newArr = [...arrSet]
console.log(newArr) //[ 33, 10, 26, 30 ]
// 4.size属性
console.log(arrSet.size) //4
// 5.Set的方法
// add
arrSet.add(100) //Set(5) { 33, 10, 26, 30, 100 }
console.log(arrSet)
// delete
arrSet.delete(33)
console.log(arrSet)
// has
console.log(arrSet.has(100)) //true
// clear
// arrSet.clear()
console.log(arrSet)
// 6.对Set进行遍历
arrSet.forEach(item => {
console.log(item)
})
for (const item of arrSet) {
console.log(item)
}
12.WeakSet使用
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
那么和Set有什么区别呢?
?区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
WeakSet常见的方法:
- add(value):添加某个元素,返回WeakSet对象本身;
- delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;
- has(value):判断WeakSet中是否存在某个元素,返回boolean类型;
WeakSet不能遍历
- 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。
- 所以存储到WeakSet中的对象是没办法获取的
13.Map的基本使用
另外一个新增的数据结构是Map,用于存储映射关系。?
但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
- 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
- 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;?
那么我们就可以使用Map
- JavaScript中对象中是不能使用对象来作为key的
- .Map就是允许我们对象类型来作为key的
Map的常用方法
Map常见的属性
Map常见的方法:
- set(key, value):在Map中添加key、value,并且返回整个Map对象;
- get(key):根据key获取Map中的value;
- has(key):判断是否包括某一个key,返回Boolean类型;
- delete(key):根据key删除一个键值对,返回Boolean类型;
- clear():清空所有的元素;
- forEach(callback, [, thisArg]):通过forEach遍历Map;
Map也可以通过for of进行遍历
// 1.JavaScript中对象中是不能使用对象来作为key的
const obj1 = { name: "why" }
const obj2 = { name: "kobe" }
// const info = {
// [obj1]: "aaa",
// [obj2]: "bbb"
// }
// console.log(info)
// 2.Map就是允许我们对象类型来作为key的
// 构造方法的使用
const map = new Map()
map.set(obj1, "aaa")
map.set(obj2, "bbb")
map.set(1, "ccc")
console.log(map)
const map2 = new Map([[obj1, "aaa"], [obj2, "bbb"], [2, "ddd"]])
console.log(map2)
// 3.常见的属性和方法
console.log(map2.size)
// set
map2.set("why", "eee")
console.log(map2)
// get(key)
console.log(map2.get("why"))
// has(key)
console.log(map2.has("why"))
// delete(key)
map2.delete("why")
console.log(map2)
// clear
// map2.clear()
// console.log(map2)
// 4.遍历map
map2.forEach((item, key) => {
console.log(item, key)
})
for (const item of map2) {
console.log(item[0], item[1])
}
for (const [key, value] of map2) {
console.log(key, value)
}
WeakMap的使用
和Map类型相似的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。?
那么和Map有什么区别呢??
区别一:WeakMap的key不能使用基本数据类型,只能使用对象,不接受其他的类型作为key;
?区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
WeakMap常见的方法有四个:
- set(key, value):在Map中添加key、value,并且返回整个Map对象;
- get(key):根据key获取Map中的value;
- ?has(key):判断是否包括某一个key,返回Boolean类型;
- delete(key):根据key删除一个键值对,返回Boolean类型
WeakMap也是不能遍历的
- 因为没有forEach方法,也不支持通过for of的方式进行遍历
|