一 原型和原型链
1.1 原型
1.1.1 原型的概念
在java 中,我们知道一个类是一种事物的抽象,通过这个类可以生成一个个具体的实例对象。我们可以理解为,类提供着生成对象的"模版 "。在 JavaScript 中构造函数(constructor)就起着"模板"的作用。 注意:构造函数的首字母大写,这是约定。
//...构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
通过构造函数,我们可以生成实例化的对象。
let p1 = new Person("张三", 21)
let p2 = new Person("李四", 18)
...
在js中,我们也知道,函数也是一个对象,那么函数这个对象的模版 是谁呢,其实就是我们要讲的原型这个东西。
1.1.2 怎么获取原型
1)使用构造函数的属性prototyoe
function Person(name, age) {
this.name = name;
this.age = age;
}
console.dir(Person);
打印构造函数时,我们可以看到里面有好几个属性,其中一个prototype,这个属性指向的就是构造函数的原型。该属性的访问,使用构造函数名.属性
的方式来访问,比如该案例中的构造函数Person的访问格式:Person.prototype
。 或者按照下图方式也可以看到。这个属性,我们也称之为显式原型
其他属性如下:
arguments: 就是用来存储调用函数时的形参的,是一个伪数组对象
caller: 指向的是本函数的调用者,也就是谁调用本函数,就指向的谁
length: 该函数的参数个数
name: 该函数的名字
2)使用实例的隐藏属性__proto__
使用构造函数创建的每一个对象里,都隐藏着一个属性__proto__
,该属性指向的也是构造函数的原型,我们也称之为隐式原型
==但是这种获取方式,在生产环境中慎用,因为并不是所有的浏览器都支持这个属性,这里我们只是学习和了解。==
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype);
let p1 = new Person('张三', 21);
console.log(p1.__proto__);
console.log(p1.__proto__ === Person.prototype); //true
3)使用Object.getPrototypeOf()方法
这个是最直接的方法,它返回指定对象的原型,即[[Prototype]]
。
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype);
let p1 = new Person('张三', 21);
let pro = Object.getPrototypeOf(p1);
console.log(pro === Person.prototype); //true
1.1.3 构造函数、实例、原型的关系
1.1.4 原型的作用
每一个实例对象都有自己的属性和方法,由于无法做到数据共享,导致资源的浪费。因此原型 被设计出来的作用,就是为了实现属性和方法的共享。
在原型上定义的属性和方法可以被由该构造函数创建出来的所有对象实例共享。这样可以节省内存空间,不需要为每个对象都单独的设计相同的属性或者方法了。
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三", 21)
let p2 = new Person("李四", 20)
// 在原型上绑定一个gender属性, 那么所有的实例对象,也都可以`继承`这个属性
Person.prototype.gender = '女'
// 在实例对象上找该属性,如果找到了就直接使用,如果没有找到,就向上找,去实例原型上找。
console.log(p1.gender, p2.gender); // 女 女
// 在原型上绑定方法sayHi。所有的实例对象上,也都继承了这个方法
Person.prototype.sayHi = function () {
console.log("你好!我是中国人");
// console.log(this); // 函数里的this, 谁调用该函数,this就指向谁
}
p1.sayHi()
p2.sayHi()
// 通过实例修改继承原型上的属性值, 实际上是实例对象新添加了一个与原型属性同名的属性而已。实例对象并不会覆盖原型上的属性值
p1.gender = '男'
console.log(p2.gender);
// 当然,实例自己独有的属性,方法,原型上是不可能有的,也不会和其他实例共享的
p1.hello = function () {
console.log("我是实例p1的方法");
}
p1.hello()
// p2.hello() //报错 Uncaught TypeError: p2.hello is not a function
==注意:== 原型决定的是实例的初始属性和方法
1.2 原型链
1.2.1 原型链简介
当访问一个对象的属性或方法时,首先JavaScript引擎会从对象自身上去找,如果找不到,就会往原型中去找,即__proto__
,也就是它构造函数的prototype中。
如果原型中找不到呢?
因为构造函数也是对象,实例原型也是对象,他们也都有__proto__
,就会往原型上去找,这样就形成了链式的结构,称为原型链
原型链的作用
JavaScript中没有传统的类继承概念,而是通过原型链实现继承。一个对象可以通过原型链继承另一个对象的属性和方法。
1.2.2 原型链图解
二 模块化编程
2.1简介
2.1.1 没有模块化编程时的影响
- 1)引入方式 都是通过
<script src="">
标签来引入到同一个html文件中。
<script src="lib.js"></script> <!-- 第三方库 -->
<script src="utils.js"></script>
<script src="app.js"></script>
- 2)命名空间的问题
当我们引入多个js文件,不同的js文件可能会有相同的变量名字,那么后引入的就会覆盖先引入的。即所有的js文件都是再全局空间进行定义的,每个js文件没有自己私有的命名空间。
- 3)依赖关系
假如app.js文件中使用了lib.js文件中的函数,那么在html中,必须先引入lib.js,再引入app.js,保证引入顺序的正确性,才不会报错。如果文件之间的依赖关系很复杂,这样的引入顺序就非常难以维护。
2.1.2 模块化编程
模块化编程,是指将复杂的代码拆分为多个独立的模块,每个模块负责完成特定的功能。某一个模块,可以通过使用export 关键字将代码导出成为模块 ;一个模块也可以通过使用import关键字导入其他模块到该模块。
模块化编程有以下优点:
-
防止命名冲突,每个模块都有自己的命名空间
-
代码复用,每个模块可以被其他多个模块引用
-
高维护性,修改一个模块其他引用该模块的地方都改变
-
确保引入顺序的正确性,使用模块化之后一般都是在自己的中引入所依赖的模块,所以避免了依赖顺序的引入问题
2.1.3 常用的模块化范式
CommonJS: 主要用在Node.js环境下
AMD:是一种用于浏览器环境的异步模块加载规范,主要用于解决浏览器中多个脚本文件的依赖问题。
ESM(ECMAScript Modules): 是 JavaScript 的内置模块系统,它已经成为 Web 和 Node.js 的标准。
TypeScript:TypeScript 是 JavaScript 的超集,它提供了静态类型检查和其他高级特性,非常适合模块化开发。
2.2 ESM范式的语法
模块功能主要由两个命令构成:export和 import
-
export
命令:导出(暴露)模块,可以让其他模块来发现和引入该模块。 有三种暴露方式1. 分别暴露 2. 统一暴露 3. 默认暴露
-
import
命令:导入(引入)其他模块的功能到该模块(文件)中
2.2.1 export暴露模块
1)分别暴露: 就是在准备暴露的数据前面,添加export关键字
a.js
//分别暴露
export let name = '李白'
export const job = '诗人'
export const person = { name: '张飞', job: '辅助', sayHi: function () { console.log(this.name, this.job); } }
export function isPrime(num) {
let f = true;
for (let i = 2; i < num - 1; i++) {
if (num % i == 0) {
f = false;
}
}
if (f) {
console.log(num, "是素数");
} else {
console.log(num, "不是素数");
}
}
2)统一暴漏: export {变量名1,变量名2,......}
b.js
const isMarry = true;
const student = { name: "小明", age: 21, gender: '男' }
function factorial(num) {
if (num == 1) {
return num;
}
return num *= factorial(num - 1)
}
export { isMarry, student, factorial }
3)默认暴露 只能暴露一个对象(只能写一次)
c.js
//第二种写法,直接在对象前面书写
//const obj = {
export default {
name: "牛魔",
age: 21,
hobbies: ['sport', 'book'],
fs: { f1() { console.log("我是第一个函数"); }, f2() { console.log("我是第二个函数"); } }
}
// 第一种写法:
//export default obj;
2.2.2 import引入模块
1)通用导入方式
- 格式:
import * as 别名 from "js文件地址"
- 该方式可以应对所有暴露方式
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 别忘记加type='module', 否则浏览器不认为是模块导入,不识别import语法 -->
<script type="module">
// 第一种方式,通用导入方式,起别名
import * as m1 from './a.js'
import * as m2 from './b.js'
import * as m3 from './c.js'
console.log(m1.name);
console.log(m1.person.name);
m1.isPrime(5)
console.log(m2.student);
let result = m2.factorial(4)
console.log('4的阶乘:', result);
//访问默认导出的东西,需要添加.default关键字
console.log(m3.default.hobbies[1]);
m3.default.fs.f1();
</script>
</body>
</html>
2)解构赋值方式
- 适用于分别暴露、统一暴露的语法
import {变量名1,变量名2,......} from "js文件地址"
注意:变量名必须和js文件中的变量名一致。
可以使用别名
import {变量名1 as 别名1,变量名2 as 别名2,......} from "js文件地址"
- 默认暴露语法
import {default as 别名} from "js文件地址"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 别忘记加type='module', 否则浏览器不认为是模块导入,不识别import语法 -->
<script type="module">
import { name, job, person, isPrime } from './a.js'
import { factorial, student } from './b.js'
import { default as m3 } from './c.js'
console.log(name);
console.log(person.name);
isPrime(5)
console.log(student);
let result = factorial(4)
console.log('4的阶乘:', result);
//访问默认导出的东西,需要添加.default关键字
console.log(m3.hobbies[1]);
m3.fs.f1();
</script>
</body>
</html>
3)简单形式
只针对默认暴露有效
import 变量名 from "js文件地址"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 别忘记加type='module', 否则浏览器不认为是模块导入,不识别import语法 -->
<script type="module">
import m3 from './c.js'
console.log(m3.hobbies[1]);
m3.fs.f1();
</script>
</body>
</html>
2.2.3 入口文件
之前我们模块的引入都是在script中写的。现在我们习惯把所有的模块引入放到一个JS文件(比如main.js )中,叫做入口文件 ,然后html里再引入入口文件,别忘记type='module'
注意: 入口文件的用途,是在HTML中引用,让HTML可以识别到所有的js模块。 并不是让我们在html里的script中书写模块里的函数,或者变量等
main.js
import { name, job, person, isPrime } from './a.js'
import { factorial, student } from './b.js'
import m3 from './c.js'
html中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 别忘记加type='module', 否则浏览器不认为是模块导入,不识别import语法 -->
<script type="module" src='main.js'>
// 不要写js代码
</script>
</body>
</html>
三 代理模式
3.1 数据代理简介
所谓数据代理(也叫数据劫持),指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
比较典型的是 Object.defineProperty()
和 ES2015 中新增的 Proxy
对象。另外还有已经被废弃的 Object.observe()
,废弃的原因正是 Proxy
的出现,因此这里我们就不继续讨论这个已经被浏览器删除的方法了。
数据劫持最著名的应用当属双向绑定(面试必考题)。
Vue 2.x 使用的是 Object.defineProperty()
Vue3.x 版本之后改用 Proxy
进行实现
3.2 Object.defineProperty()
3.1.1 简介
现在我们来学习一个功能更加强大的方式,那就是使用Object.defineProperty(),该方法可以在对象上新添加属性,或者修改已经存在的属性。它也是vue2的响应式原理。
默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举(遍历)和不可配置(删除或修改)的。但是我们可以通过参数来改变这些。
语法:
Object.defineProperty(obj, prop, desc)
参数:
obj: 要操作的对象
prop: 将要新添加或者修改的属性名 「String、Symbol类型」
desc: 属性描述符,通过它来限制属性的读写行为
属性描述符
-
value:设置属性的值
-
writable:值是否可以重写,默认值为false
-
set:目标属性设置值的方法
-
get:目标属性获取值的方法
-
enumerable:目标属性是否可以被枚举(是否可以遍历),默认值为false
-
configurable:目标属性是否可以被删除或是否可以再次通过属性描述符的方式修改,默认值为false
属性描述符分为两种:「数据描述符」和「存取描述符」。
其中value、writable为数据描述符,get、set为存取描述符,configurable 和 enumerable不受限制。
3.1.2 案例演示:添加对象属性
以前,我们在js中给对象添加属性、方法时,我们通常都是这样做的
// 定义对象时,添加属性和属性值
let person = { name: 'zhangsan', age: 21 };
// 定义对象后,添加新属性和属性值
person.gender = '男';
// 定义对象后,添加新方法
person.sayHi = function () {
console.log(`你好,我是${this.name}`);
}
console.log(person.name, person.age, person.gender);
person.sayHi()
console.log(person);
测试:value,writable,enumerable,configurable四个属性
let human = { name: '张三' }
Object.defineProperty(human, 'age', {
value: 21, //使用value进行赋值
writable: true, // 注释和不注释,查看效果
enumerable: true,
configurable: true
})
console.log(human); //查看对象
human.name = "张大三"
// -----测试1:修改属性的值
human.age = 22; //修改age的值
console.log(human); //再次查看对象
// -----测试2:枚举对象的属性
console.log(Object.keys(human));
for (key in human) {
console.log(key);
}
// -----测试3.1 删除属性
delete human.name;
console.log(human);
delete human.age
console.log(human);
// -----测试3.2 修改属性
Object.defineProperty(human, 'age', {
value: 30
})
console.log(human);
3.1.3 响应式页面设计
1)什么是响应式
在前端开发中,响应式原理是一个非常重要的概念。它允许我们的应用程序在用户与界面交互时,动态地更新视图。这种交互性使得我们的应用程序能够实时地响应用户的操作。
响应式原理的核心思想是:当数据发生变化时,能够自动地更新视图。为了实现这一目标,我们需要对数据的变化进行监听,并在数据发生变化时触发视图的更新。
2)案例演示
let person = { name: 'zhangsan', age: 21 }
let nameDom = document.getElementById("span1")
nameDom.innerText = person.name
let ageDom = document.getElementById("span2")
ageDom.innerText = person.age;
//更新对象的属性,比如name,age等
person.name = "李四"
person.age = 30;
//更新完属性的值,页面并没有跟着改变,这就不是响应式。 我们需要手动修改才行
nameDom.innerText = person.name
ageDom.innerText = person.age;
// 现在想要完成响应式效果,我们需要借用Object.defineProperty.....和一个变量。
let number = 0
Object.defineProperty(person, 'age', {
get: function () {
console.log("-----age被访问了-----");
return number;
},
set: function (value) {
console.log("-----age被修改了" + value);
// 修改时,页面就直接修改了
ageDom.innerText = value;
number = value
},
})
let str = '赵老六'
Object.defineProperty(person, 'name', {
get: function () {
console.log("-----name被访问了-----");
return str;
},
set: function (value) {
console.log("-----name被修改了" + value);
// 修改时,页面就直接修改了
nameDom.innerText = value;
str = value
},
})
console.log(person);
3.1.4 数据代理(双向绑定)
class ProxyObject {
constructor(obj) {
this._data = obj;
// 遍历对象的所有属性
for (let key in obj) {
// 判断key是不是自身的属性,而不是从原型链上继承过来的属性
if (obj.hasOwnProperty(key)) {
// 数据劫持,数据代理
Object.defineProperty(this, key, {
get: function () {
// 有人访问obj的属性,我们就返回代理对象ProxyObject的属性值
return this._data[key];
},
set: function (newValue) {
// 有人修改了obj的属性,我们也修改代理对象属性的值
this._data[key] = newValue;
}
});
}
}
}
}
// 使用示例
let data = { name: 'tom', age: 30 };
let proxy = new ProxyObject(data);
console.log(proxy.name); // 输出:tom
proxy.name = 'michael'; // 修改代理对象的属性,实际上修改的是原始数据对象
console.log(data.name); // 输出:michael
data.name = "lucy"
console.log(proxy.name);
3.1.5 做数据代理的缺点
- 不能监听数组的变化,数组的以下几个方法不会触发 set:
push pop shift unshift splice sort reverse
-
必须遍历对象的每个属性
-
必须深层遍历嵌套的对象
3.3 Proxy
3.3.1 简介
Proxy
是ES6引入的一个新对象,用于创建一个对象的代理,可以拦截并重定义基本的操作。
在数据劫持这个问题上,Proxy
可以被认为是 Object.defineProperty()
的升级版。外界对某个对象的访问,都必须经过这层拦截。因此它是针对 整个对象 ,而不是 对象的某个属性 ,所以也就不需要对 keys
进行遍历。
语法:
const p = new Proxy(target, handler)
-
target :目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
-
handler :以函数作为属性的对象,实现拦截和自定义操作。
handler对象是一个容纳一批特定属性的占位符对象,它包含有Proxy的各个捕获器trap。所有的捕捉器是可选的,如果没有定义某个捕捉器,那么就会保留源对象的默认行为
handler.getPrototypeOf(): Object.getPrototypeOf方法的捕捉器。
handler.setPrototypeOf(): Object.setPrototypeOf方法的捕捉器。
handler.isExtensible(): Object.isExtensible方法的捕捉器。
handler.preventExtensions(): Object.preventExtensions方法的捕捉器。
handler.getOwnPropertyDescriptor(): Object.getOwnPropertyDescriptor方法的捕捉器。
handler.defineProperty(): Object.defineProperty方法的捕捉器。
handler.has(): in操作符的捕捉器。
handler.get(): 属性读取操作的捕捉器。
handler.set(): 属性设置操作的捕捉器。
handler.deleteProperty(): delete操作符的捕捉器。
handler.ownKeys(): Reflect.ownKeys、Object.getOwnPropertyNames、Object.keys、Object.getOwnPropertySymbols方法的捕捉器。
handler.apply(): 函数调用操作的捕捉器。
handler.construct(): new操作符的捕捉器。
3.3.2 数据代理
//定义一个学生对象
let student = { name: '王二狗', age: 18 }
//给学生对象定义一个代理对象
let p1 = new Proxy(student, {
get(target, property) {
return target[property]
},
set(target, property, value) {
target[property] = value
return true;
}
})
//改变代理对象的属性,实际上就是改变原对象的属性
p1.name = "张狗蛋"
p1.age = 21;
console.log(student);
部分方法的测试:
//定义一个学生对象
let student = { name: '王二狗', age: 18 }
//给学生对象定义一个代理对象
let p1 = new Proxy(student, {
get(target, property) {
return target[property]
},
set(target, property, value) {
target[property] = value
},
has(target, property) {
console.log("---has方法触发了---传过来的属性名:" + property);
return property in target
},
deleteProperty(target, property) {
console.log("---deleteProperty方法触发了---传过来的属性名:" + property);
delete target[property]
return true;
},
defineProperty(target, property) {
console.log("---defineProperty方法触发了---传过来的属性名:" + property);
return Object.defineProperty(target, property, {})
}
})
console.log("a" in p1);
delete p1.name
console.log(student);
Object.defineProperty(p1, 'name', {})
3.3.3 响应式页面
<div id="containner">
</div>
let person = { name: "michael" }
let div = document.getElementById("containner");
div.innerText = person.name;
person.name = "tom"
let p1 = new Proxy(person, {
get(target, property) {
return target[property]
},
set(target, property, value) {
//数据劫持,修改页面上的内容
div.innerText = value
target[property] = value
}
})
p1.name = "tom"
p1.name = "lucy"