JavaScript高级进阶(更新中)-javascript-gao-ji-jin-jie--geng-xin-zhong-(二)https://developer.aliyun.com/article/1469504
ES5&ES6对象特性
ES5
对象和函数的原型
JS中每一个对象都有一个特殊的内置属性,这个特殊的对象可以指向其他的对象
- 我们通过引用对象的属性key来获取一个value时,它会触发 Get 的操作
- 首先检查该对象是否有对应的属性,如果有的话就使用对象内的
- 如果对象中没有属性,那么会访问对象的
prototype
- 每一个对象都有一个原型属性
使用方式有两种:
- 通过对象的
_proto_
属性可以获取到(浏览器自己添加的,存在一定的兼容性问题) - 通过 Object.getPrototypeOf 方法可以获取
prototype属性是函数特有的属性 我们的对象可以通过Object.getPrototypeOf
或__proto__
来查看原型。
var obj = { } function foo() { } console.log(foo.prototype);
当我们这个对象有对多个共同值的时候,可以把相同的东西当如原型里,这样每次创建这个对象的时候,就可以直接调用而不是重新创建。
function Student(name, age) { this.name = name this.age = age // 如果我们每个对象都创建那么这两个方法会出现很多的冗余 // this.running = function () { // console.log(this.name + "running"); // } // this.eating = function () { // console.log(this.name + "eating"); // } } Student.prototype.running = function () { console.log(this.name + "running"); } Student.prototype.eating = function () { console.log(this.name + "eating"); } var stu1 = Student("jjj", 12) var stu2 = Student("hhh", 18)
Constructor属性
原型对象上面是有一个属性的:constructor,默认情况下原型都会有一个叫constructor
指向当前的对象
function Person() { } var PersonProtype = Person.prototype console.log(PersonProtype); console.log(PersonProtype.constructor); console.log(PersonProtype.constructor == Person);
原型对象是可以重写的,当我们需要给原型添加更多的属性的时候一般我们会选择重写原型对象
我们也可以改变原型对象中constructor的指向的使用
//改变指向对象 Person.prototype={ constructor:Person } //修改枚举类型 Object.defineProperty(Person.prototype,"constructor",{ enumerable:false })
这里要注意的是原生的constructor是不可枚举的,但是修改constructor的时候会让constructor的特性被设置为true这个时候需要修改一下对象默认属性设置
创建对象的内存表现:
如果我们向对象加入属性在之后的变化:
原型对象默认创建的时候,proto都是指向object的的proto的
多种继承方式
继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态
这里主要将JS中的继承,在了解继承之前我们需要了解JS中的原型链机制,这个是之后理解的关键
原型链
在js中我们不断的获取原型对象,原型链最顶层的原型对象就是Object的原型对象
[Object: null prototype] {}
这种提示一般有两个情况:
- 该对象有原型,且这个原型的属性指向null或者最顶层了
- 这个对象有很多的默认属性方法
ps:Object是所有类的父类
我们也可以对原型链做一些自定义操作,比如这样:
var obj = { } obj.__proto__ = { } obj.__proto__.__proto__ = { } obj.__proto__.__proto__.__proto__ = { name: "小冷" }
原型链实现继承
function Person(){ this.name = "l" } var p = new Person() stu.prototype = p //name == l stu.prototype.studying = function(){ console.log(this.name+"studying") }
我们可以通过赋值原型的形式来实现继承,但是有一些弊端
- 直接打印对象是看不到属性的
- 这个属性会被多个对象共享,如果是引用类型就会造成问题
- 不能给父类传递参数,没法定制化
借用构造函数继承
为了解决原型链继承中存在的问题,constructor stealing
应运而生 ,借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
- 因为函数可以任意调用
- 因此通过apply和call也可以再新创建的对象上实行构造函数
function Person(name, age, height, address) { this.name = name this.age = age this.height = height this.address = address } function Student(name, age, height, address, sno, score) { Person.call(this,name, age, height, address) this.sno = sno this.score = score }
可以使用父类的构造函数来实现创造,解决之前原型链的问题 在ES6之前一直是保持的这个方式,但是这个继承方式依然不是很完美
- 无论在什么情况下,都会调用两次父类构造函数。 一次是创建子类原型,一次是构造函数
- 所有的子类都会有两份父类的属性
继承最终方案
在继续的发展中, JSON的创立者道格拉斯, 提到了新的继承方法,这也是目前es5 阶段最合适的继承方案 寄生组合继承
- 结合原型类继承和工厂模式
- 创建一个封装继承过程的函数,在这个函数的内部来增强对象,最后将这个对象返回
function Person(name, age, height, address) { this.name = name this.age = age this.height = height this.address = address } Person.prototype.running = function () { console.log(this.name + " running"); } function Student(name, age, height, address, sno, score) { Person.call(this, name, age, height, address) this.sno = sno this.score = score } // 原型继承 var obj = Object.create(Person.prototype) console.log(obj.__proto__ === Person.prototype); Student.prototype = obj // 上到真是环境 会封装用 为了兼容性可以多一个创造类的方法 function object(o){ function F(){} F.prototype = o return new F() } function inherit(Subtype, Supertype) { Subtype.prototype = object(Supertype.prototype) // 需要构造方法 Object.defineProperty(Subtype, "constructor", { enumerable: false, configurable: this, writable: true, value: Subtype }) } inherit(Student, Person) Student.prototype.eating = function () { console.log(this.name + "eating"); } var stu = new Student("小明"); stu.eating()
对象方法补充
hasOwnProperty : 对象是否有某一个属于自己的属性
in/for in 操作符: 判断某个属性是否在对象或者对象的原型上
instanceof : 用于检测构造函数的原型,是否出现在某个实例对象的圆形脸上
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上
ES6
class定义关键字
这个关键字主要是用于区别代码的编写方式的,之前编写中创建类和构造函数过于相似,而且代码并不容易理解
- 在ES6的标准中使用了class关键字来直接定义类
- 类本质上依然是前面所讲的构造函数、原型链的语法糖
- 比较少用 一般情况遇不到
使用方法
class Person{} var Student = class{ }
类的构造函数
在创建对象的时候给类传递一些参数
- 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor
- 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor
- 构造函数时唯一的 不能出现多个
通过new关键字操作类的时候,会调用这个constructor函数
- 在内存中创建一个新的空对象
- 对象内部的[[prototype]]属性会被赋值为该类的prototype属性
- 构造函数内部的this,会指向创建出来的新对象
- 执行构造函数的内部代码
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
语法使用
class Person { constructor(name, age) { this.name = name this.age = age } running() { console.log(this.name + "running"); } eating() { console.log(this.age + "eating"); } } var p1 = new Person("123", 123) console.log(p1.name, p1.age)
class 创建的类和function直接创造构造方法创建的类时没有什么区别的
定义访问器
对象的属性描述符时有讲过对象可以添加setter和getter函数的,类同样也是可以的
var ojb = { _name:"why", set name(value){ this._name = value }, get name(){ return this._name } }
类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
class Person{ constructor(age){ this.age = age } static create(){ return new Person(Math.floor(Math.random()*100)) } }
extends 继承
ps : js的继承属于只支持单继承
之前我们在es5 中经过原型链继承,组合继承等等操作才解决继承的一些问题,但是在ES6 中 他给我们提供了一个关键字 : extends
class Person { constructor(name, age) { this.name = name this.age = age } running() { console.log(this.name + "running"); } eating() { console.log(this.age + "eating"); } } class Teacher extends Person { constructor(name, age, title) { // this.name = name // this.age = age super(title) this.title = title } Teaching() { console.log(this.name + "Teaching"); } } class Student extends Person { constructor(name, age, sno) { // this.name = name // this.age = age super(name, age) this.sno = sno } studying() { console.log(this.name + "studying"); } } var p1 = new Student("123", 123, "123") console.log(p1.name, p1.age) p1.eating()
我们只需要在类之后用extends 指向需要被继承的类 就可以实现继承
Super 关键字
Class为我们的方法中还提供了super关键字
- 执行 super.method(...) 来调用一个父类方法
- 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)
- super使用的位置有三个 子类构造函数,实例方法,静态方法
PS: 在子类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数
在JS 中 我们子类也是可以重写父类的方法的,但是当我们既想让子类有自己的操作,还想复用父类的实现,就可以使用super 关键字
class Animal { running() { console.log("running"); } } class dog extends Animal { running() { console.log("dog four jio"); super.running() } } var dog = new dog() dog.running()
继承内置类
同样 我们可以继承内置类,比如数组 Array 类 加入我们想给数组加一些拓展比如获得第一位 获得最后一位 我们就可以继承数组对象添加我们想要定义的拓展
// 继承内置类 class CodeArray extends Array { get lasItem() { return this[this.length - 1] } get firstItem() { return this[0] } } var arr = new CodeArray(12, 14, 15, 16, 20) console.log(arr); console.log(arr.lasItem());
类的混入mixin
JavaScript的类只支持单继承,混入的思想可以帮助我们利用函数的方式实现嵌套继承,
- 它可以通过编写混入函数以返回新对象的方式实现多个继承
class Person { constructor(name, age) { this.name = name this.age = age } swimming() { console.log(first) } } function mixinRunner(BaseClass) { return class extends BaseClass { running() { console.log(this.name + " running~") } } } function mixinEater(BaseClass) { return class extends BaseClass { eating() { console.log(this.name + " eating~") } } } class NewPerson extends mixinEater(mixinRunner(Person)) { } var NP = new NewPerson(); NP.swimming()
ES6新的语法糖
ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals
主要包括:
- 属性的简写:Property Shorthand
- 方法的简写:Method Shorthand
- 计算属性名:Computed Property Names
属性的简写
当属性和变量名字完全一样的时候 可以将他省略 之后生成的代码还是和之前无差
/* 1. 属性的简写 */ var name = "123" var age = 12 var obj = { name, age }
方法的简写
/* 1. 属性的增强 */ var obj = { running:function(){}, //简写为 running(){} }
计算属性名
var address = "us" var obj = { [address]: "北京" }
解构 Destructuring
ES6中新增了一个从数组或对象中方便获取数据的方法,它是一种特殊语法
数组的解构:
- 基本解构过程
- 顺序解构
- 解构出数组:…语法
- 默认值: undefind
var names = ["nnn","qqq","lll","fff"] var [names1,names2,...names] = names //输出结果 nnn,qqq,[lll,fff]
对象的解构:
- 基本解构过程
- 任意顺序
- 重命名
- 默认值
var obj = {name:"uuu",age:12,height:1.88} var {height,name} = obj //需要重命名 var {height:hei,name} = obj 默认值 var {name,age,grilfrineds:gf = "lucy"} = obj
对象解构的应用:
function getPostion({x,y}){ var a = x+y } getPostion({x:10,y:20});
实现函数 apply /call / bind
实现函数内容
首先我们要遵循封装的思想 , apply 和 call 方法 其实就是调用的方式不同而已
所以我们可以将这两个方法调用的共同点封装成一个函数 接下来只需要用不同的方式调用就可以了
function foo(name, ) { console.log(this, name, age); } // foo.apply("aaa", ["hyc", 12]) // foo.call("aaa", "lebron", 38) function execFn(thisArg, otherArgs, fn) { // 1、 获取 thisArg 确保是一个对象类型 thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg) // thisArg 传入的第一参数是要绑定的this Object.defineProperty(thisArg, "fn", { enumerable: false, configurable: true, value: fn }) thisArg.fn(...otherArgs) delete thisArg.fn } Function.prototype.hycApply = function (thisArg, otherArgs) { execFn(thisArg, otherArgs, this) } Function.prototype.hyccall = function (thisArg, ...otherArgs) { execFn(thisArg, otherArgs, this) } foo.hycApply({ name: "hyc" }, ["james", 25]) foo.hyccall(123, "why", 18) Function.prototype.hycbind = function (thisArg, ...otherArgs) { thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg) Object.defineProperty(thisArg, "fn", { enumerable: false, configurable: true, writable: false, value: this }) return (...newArgs) => { // var allArgs = otherArgs.concat(newArgs) var allArgs = [...otherArgs, ...newArgs] thisArg.fn(...allArgs) } } var newFoo = foo.hycbind("abc", "hyc", 30) // newFoo(1.88,"广州")、 newFoo()
ES6-ES13
es6
JS代码执行过程中需要了解的ECMA文档的术语
- 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
- 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
- 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
- 全局对象:Global Object,全局执行上下文关联的VO对象;
- 激活对象:Activation Object,函数执行上下文关联的VO对象;
- 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
let/const基本使用
ES6开始新增了两个关键字可以声明变量:let、const
- let、const不允许重复声明变量
- let 不会作用域提升
- let、const在执行声明代码前是不刻意访问的
var、let、const的选择
- 在未来的开发中 很少会使用var 来声明变量来开发了
- let const 比较推荐在开发中使用
- 推荐优先 使用 const 保证数据的安全性不会被随意的篡改
- 只有需要重复赋值的时候才使用 let
模板字符串
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接,使用 ``` `符号来编写字符串,称之为模板字符串
可以在模板字符串的时候用 ${}
来嵌入动态内容
``` ` 符号还可以调用方法自动传入参数
const name = "hyc" const age = 18 function foo(...args){ console.log("111",args) } foo `my name is ${name},age is ${age},height is ${1.88}`
函数默认值
ES6 之后 函数允许给参数一个默认值
function test(x = 10, y = 10)
可以使用这种方式来给函数的参数加入默认值,允许我们使用表达式比如
x = 1 || x > 1
Symbol的基本使用
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
- 比如在新旧值同名的情况下混入 会有一个值被覆盖掉
- 比如之前有的fn 属性 如果新添加一个fn 属性 内部原有的fn 会怎么样?
Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
- 在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol() const s2 = Symbol() const obj = { [s1]: "aaa", [s2]: "aaa" } // 获取 symbol 值 对应的key console.log(Object.getOwnPropertySymbols(obj));
Set的基本使用
在ES6之前,我们存储数据的结构主要有两种:数组、对象。
在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。Set 中数据是不能重复的
常用方法
set 支持for of
Set常见的属性:
- size:返回Set中元素的个数;
Set常用的方法:
- add(value):添加某个元素,返回Set对象本身;
- delete(value):从set中删除和这个值相等的元素,返回boolean类型;
- has(value):判断set中是否存在某个元素,返回boolean类型;
- clear():清空set中所有的元素,没有返回值;
- forEach(callback, [, thisArg]):通过forEach遍历set;
// 创建 set const set1 = new Set() set1.add(11) set1.add(12) set1.add(13) set1.add(11) console.log(set1.size);
WeakSet使用
WeakSet和Set有什么区别
- WeakSet 不可以遍历
- WeakSet中只能存放对象类型,不能存放基本数据类型
- WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
let obj1 = { name: "why" } let obj2 = { name: "hyc" } const weakset = new WeakSet() weakset.add(obj1) weakset.add(obj2)
Map基本使用
Map,用于存储映射关系
之前我们可以使用对象来存储映射关系,他们有什么区别
- 在之前的学习中对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key)
- 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
这个时候就可以使用 map
const info = { name: "hyc" } const map = new Map() map.set(info, "aaaa")
Map的常用方法
Map常见的属性:
- size:返回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;
WeakMap的使用
WeakMap,也是以键值对的形式存在的。
WeakMap和 map 的区别
- WeakMap的key只能使用对象,不接受其他的类型作为key;
- WeakMap的key对对象想的引用是弱引用
- WeakMap不能遍历
WeakMap常见的方法有四个:
- set(key, value):在Map中添加key、value,并且返回整个Map对象;
- get(key):根据key获取Map中的value;
- has(key):判断是否包括某一个key,返回Boolean类型;
- delete(key):根据key删除一个键值对,返回Boolean类型;
拓展知识
数值的表示
允许使用二进制 八进制来赋值
const num1 = 100 const num2 = 0b100
ES2021 新增数字过长可以用 _ 来连接
const num1 = 100_000_000