赞
踩
文章根据codewhy老师的课程学习整理深入JavaScript高级语法-coderwhy大神新课-学习视频教程-腾讯课堂
我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。
在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
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:
1.在内存中创建一个新的对象(空对象);
2.这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
3.构造函数内部的this,会指向创建出来的新对象;
4.执行构造函数的内部代码(函数体代码);
5.如果构造函数没有返回非空对象,则返回创建出来的新对象;
在上面我们定义的属性都是直接放到了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)
我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的:
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用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中新增了使用extends关键字,可以方便的帮助我们实现继承:
在子(派生)类的构造函数中使用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())
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()
不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现
那么从上面的定义来看,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)
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'
- // }
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
-
- //重命名成newName
- 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
解构目前在开发中使用是非常多的:
在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
注意事项一: const本质上是传递的值不可以修改
但是如果传递的是一个引用类型(内存地址), 可以通过引用找到对应的对象, 去修改对象内部的属性, 这个是可以的
1. 通过let/const定义的变量名是不可以重复定义
2. let、const没有进行作用域提升,但是会在解析阶段被创建出来
3. Window对象添加属性
也就是说我们声明的变量和环境记录是被添加到变量环境中的
4.块级作用域区别
ES5中没有块级作用域,只有两个东西会形成作用域
在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的
但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的: p这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升
var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些 历史遗留问题
在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;
对于let、const:
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
- // 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`
在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:
而在ES6中,我们允许给函数一个默认值:
默认值也可以和解构一起来使用
另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的)
另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了、
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
那么剩余参数和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
- 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: '广州市'
- // }
展开运算符是浅拷贝,他第一层是深拷贝,互不影响,如果有对象,里面的对象是相同的内存地址,是浅拷贝,所以总体是浅拷贝
在ES6中,引用赋值是指将一个变量的值赋给另一个变量时,两个变量实际上是指向同一个内存地址,即它们引用的是同一个对象或值。
在JavaScript中,基本类型(如数字、字符串、布尔值等)是按值传递的,而对象类型(如数组、对象、函数等)是按引用传递的。这意味着,当你将一个对象赋值给另一个变量时,实际上是将对象的引用赋值给另一个变量,而不是创建一个新的对象副本。
- let obj1 = { name: "John" };
- let obj2 = obj1; // 引用赋值
-
- obj2.name = "Jane";
- console.log(obj1.name); // 输出 "Jane"
在这个例子中,obj1
和 obj2
指向同一个对象,修改 obj2
的属性也会影响到 obj1
。
需要注意的是,对于基本类型,赋值操作会创建一个新的值副本,修改其中一个变量不会影响另一个变量。
- let obj1 = { name: "John", age: 25, address: { city: "New York" } };
- let obj2 = Object.assign({}, obj1); // 浅拷贝
-
- obj2.address.city = "Los Angeles";
- console.log(obj1.address.city); // 输出 "Los Angeles"
- let obj1 = { name: "John", age: 25, address: { city: "New York" } };
- let obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
-
- obj2.address.city = "Los Angeles";
- console.log(obj1.address.city); // 输出 "New York"
- 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)
在ES6中,Symbol是一种新的数据类型,用于创建独一无二的标识符。它的基本使用方式如下
创建Symbol类型:可以使用Symbol()
函数来创建一个Symbol类型的值。
作为属性名:由于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
在ES6之前,我们存储数据的结构主要有两种:数组、对象
Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。
我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重
Set常见的属性:
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)
- }
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
那么和Set有什么区别呢?
区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
WeakSet常见的方法:
WeakSet不能遍历
另外一个新增的数据结构是Map,用于存储映射关系。
但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
那么我们就可以使用Map
Map的常用方法
Map常见的属性
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)
- }
和Map类型相似的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。
那么和Map有什么区别呢?
区别一:WeakMap的key不能使用基本数据类型,只能使用对象,不接受其他的类型作为key;
区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
WeakMap常见的方法有四个:
WeakMap也是不能遍历的
ES11:globalThis
ES11: for...in标准化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。