js组成
stateDiagram-v2
JavaScript --> ECMAScript(js语言基础)
JavaScript --> webAPIs
webAPIs --> DOM(页面文档对象模型)
webAPIs --> BOM(浏览器对象模型)
js权威网站:https://developer.mozilla.org/zh-CN/
直接搜索‘mdn’
DOM:是浏览器提供的用来操作网页内容的功能
DOM树是什么?
- 将 HTML文档 以树状结构直观的表现出来,我们称之为 文档树 或 DOM 树
- 描述网页内容关系的名词
- 作用:
文档树直观的体现了标签与标签之间的关系
DOM对象
- 浏览器根据html标签生成的
js对象 - 所有的标签属性都可以在这个对象上面找到
- 修改这个对象的属性会自动映射到标签身上
自定义属性
- 在html5中推出来了专门的
data-自定义属性 - 在标签上一律以
data-开头 - 在DOM对象上一律以
dataset对象的方式获取
<div class='box' data-id='1' data-value='22'>自定义属性盒子</div>
const box = document.querySelector('.box');
console.log(box.dataset) // 打印DOM集合 {id: 1, value: 22}
键盘监听
input.addEventListener('keyup', function (e) {
if (e.key === 'Enter') {
console.log('按下了回车键');
}
})
事件委托
- 是利用事件流的特征解决一些开发需求的知识技巧
- 优点:减少注册次数,可以提高程序性能
- 原理:利用
事件冒泡的特点- 给
父元素注册事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件。
- 给
- 实现:
事件对象.target.tagName可以获得真正触发事件的元素。
作用域
- 局部作用域:包括
函数作用域和块作用域- 块作用域:在
javascript中使用{}包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
1、
let声明的变量、const声明的常量 都会产生块作用域,var不会产生块作用域。2、不同代码块之间互不影响
- 块作用域:在
- 全局作用域:
<script>标签和.js文件的【最外层】就是所谓的全局作用域
:应该尽可能少的声明全局变量,防止全局变量被污染。
作用域链:本质上是底层的变量查找机制。
- 在代码被执行时,会
优先查看当前作用域中查找变量 - 如果当前作用域查找不到,则会依次
逐级查找父级作用域直到全局作用域。
:
1、嵌套关系的
作用域串联起来就形成了作用域链2、相同作用域链中按照从小到大的规则查找变量
3、子作用域能够访问父作用域,父作用域无法访问子作用域。
垃圾回收机制
- 内存的生命周期:js环境中分配的内存,一般有如下
生命周期:内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存。内存使用:即读写内存,也就是使用变量、函数等。内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存。
- 说明:
- 全局变量一般不会回收(关闭页面回收)
- 一般情况下
局部变量的值不用了,会被自动回收掉
- 内存泄漏:指程序中分配的
内存由于某种原因,程序未释放或无法释放。 - 算法说明:堆栈空间分配区别
- 栈(操作系统):由
操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。 - 堆(操作系统):一般由程序员分配释放,若程序员不释放,由
垃圾回收机制回收。复杂数据类型放到堆里面。 - 两种常见的浏览器
垃圾回收算法:引用计数法和标记清除法。
1、引用计数
IE采用的引用计数算法,定义“
内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象。-
算法:
1)跟踪记录被
引用的次数2)如果被引用了一次,那么就记录次数1,多次引用会
累加++3)如果减少一个引用就
减1 --4)如果引用次数是
0,则释放内存 -
缺点:
嵌套引用(循环引用)如果两个对象
相互引用,尽管他们已不再使用,垃圾回收器也不会进行回收,导致内存泄漏。function fn() { let o1 = {}; let o2 = {}; o1.a = o2; o2.a = o1; return '引用计数无法回收'; // 因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在,就会导致大量的内存泄漏 } fn();
2、标记清除法
现代的浏览器已经不再使用引用计数算法了,现代浏览器通用的大多是基于
标记清除算法的某些改进算法,总体思想都是一致的。-
核心:
1)标记清除算法将“不再使用的对象”定义为“
无法达到的对象”2)就是从
根部(在js中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。3)那些
无法由根部出发触及到的对象被标记为不再使用,稍后进行回收
- 栈(操作系统):由
标记所有的引用
闭包
- 概念:一个函数对周围状态的引用捆绑在一起,内存函数中访问到其外层函数的作用域。即【闭包 = 内层函数 + 外层函数的变量】
- 作用:封闭数据,提供操作,外部也可以访问函数内部的变量
- 应用:实现数据的私有
function outer() {
const a = 1;
function f() {
console.log(a)
}
f();
}
outer();
// 常见的闭包的形式, 外部可以访问或者使用 函数内部的变量
function outer() {
let a = 10;
function f() {
console.log(a)
}
return f
}
// outer() 等价于 f => 等价于 function f() {}
const fun = outer();
fun();
创建对象的三种方式
- 利用对象字面量创建对象
const o = { name: '章三' }
- 利用
new Object创建对象
const obj = new Object();
obj.name = '章三';
const obj1 = new Object({ name: '章三' })
- 利用构造函数创建对象
- 构造函数:是一种特殊的函数,主要用来初始化对象。
- 使用场景:常规的
{...}语法允许创建一个对象。比如:我们创建了对象A,继续创建对象B 还需要重新写一遍,此时,可以通过构造函数来快速创建多个类似的对象。
// 创建对象A
const A = { name: '章三', age: 18 }
// 创建对象B
const B = { name: '里斯本', age: 20 }
=====================================
// 构造函数快速创建
function ComObj (name, age) {
this.name = name;
this.age = age;
}
// 创建对象A
const A = new ComObj('章三', 18);
// 创建对象B
const B = new ComObj('里斯本', 20);
构造函数在技术上是常规函数。
不过有两个约定:
命名以大写字母开头
只能由
new操作符来执行说明:
- 使用
new关键字调用函数的行为被称为实例化- 实例化构造函数时,没有参数时可以省略(),但是不提倡省略
- 构造函数内部无需写
return,返回值即为新创建的对象- 构造函数内部的
return,返回的值无效,所以不要写returnnew Object()、new Date()也是实例化构造函数实例化执行过程(new的过程发生了什么?)
- 创建一个新的空对象
- 构造函数的
this指向新对象- 执行构造函数代码,修改
this,添加新的属性- 返回新对象
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)
说明:
- 为构造函数传入参数,创建
结构相同但值不同的对象- 构造函数创建的实例对象
彼此独立互不影响
function Person() {
// 构造函数内部的 this 就是 实例对象
// 实例对象中动态添加属性
this.name = '章三';
// 实例对象动态添加方法
this.say = function () {
console.log('hello~')
}
}
// 实例化, p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person();
console.log(p1);
console.log(p1.name) // 访问实例属性
p1.say() // 调用实例方法
静态成员
构造函数的属性和方法被称为静态成员(静态属性和静态方法)
说明:
- 静态成员只能构造函数来访问
- 静态方法中的
this指向构造函数比如:
Date.now()、Math.PI、Math.random()常用的静态方法:
Object.assign: 常用于对象拷贝。经常使用的场景:给对象添加属性const o = { name: '章三', age: 16 } Object.assign(o, { gender: '女' }) console.log(o) // { name: '章三', age: 16, gender: '女' }
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2;
Person.arms = 2;
// 静态方法
Person.walk = function () {
console.log('hello~')
// this 指向 Person
console.log(this.eyes)
}
编程思想
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。即:以对象功能来划分问题,而不是步骤面向对象编程(oop)
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工
- 面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目
- 面向对象的特性: 1)封装性 2)继承性 3)多态性
构造函数
- 封装是面向对象思想中比较重要的一部分,
js面向对象可以通过构造函数实现封装。 - 同样的将变量和函数组合到一起并能通过
this实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的。(存在浪费内存的问题)
| 面向过程编程 | 面向对象编程 |
|---|---|
| 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如:单片机就采用的面向过程编程 | 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 |
| 缺点:没有面向对象易维护、易复用、易扩展 | 缺点:性能比面向过程低 |
原型
- 目标:利用原型对象实现方法共享
- 构造函数通过原型分配的函数是所有对象所
共享的 Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象,所以我们也称为原型对象- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。构造函数和原型对象中的this都指向实例化的对象。constructor属性
在哪里?
每个原型对象里面都有个
constructor属性(constructor构造函数)作用
该属性
指向该原型对象的构造函数Star === Star.prototype.constructor // true使用场景
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值。
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象
constructor就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个
constructor指向原来的构造函数。
对象原型
对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。:
__proto__是js非标准属性[[prototype]]和__proto__意义相同- 用来表明当前实例对象指向哪个原型对象的
prototype__proto__对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数function Star() {} const s = new Star() // 对象原型 __proto__ 指向 该构造函数的原型对象 console.log(s.__proto__ === Star.prototype) // true // 对象原型 里面有 constructor 指向构造函数 Star console.log(s.__proto__.constructor === Star) // true
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,javascript中大多是借助原型对象实现继承的特性
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
查找规则
- 当访问一个对象的属性(包括方法)时,首先查找这个
对象自身有没有该属性。- 如果没有就查找它的原型(也就是
__proto__指向的prototype原型对象)- 如果还没有就查找原型对象的原型(
Object的原型对象)- 依此类推一直找到
Object为止(null)__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线- 可以使用
instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
this
this指向- 普通函数:调用方式决定了
this的值,即谁调用 this 的值 指向谁。
普通函数没有明确调用者时,
this值为window,严格模式下没有调用者时this的值为undefined- 箭头函数:事实上
箭头函数中并不存在this
- 箭头函数会默认帮我们绑定外层
this的值,所以在箭头函数中this的值和外层的this是一样的 - 箭头函数中的
this引用的就是最近作用域中的this - 向外层作用域中,一层一层查找
this,直到有this的定义
- 在开发中【使用箭头函数前需要考虑函数中的
this的值】,事件回调函数使用箭头函数时,this为全局的window。因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数。
// DOM节点 const btn = document.querySelector('.btn'); // 箭头函数,此时 this 指向了 window btn.addEventListener('click', () => { console.log(this) }) // 普通函数,此时 this 指向了 DOM对象 btn.addEventListener('click', function () { console.log(this) })- 基于原型的面向对象也不推荐采用箭头函数
function Person() {} // 原型对象上添加箭头函数 Person.prototype.walk = () => { console.log(this) // window } const p1 = new Person(); p1.walk();- 箭头函数内不存在
this,沿用上一级的 - 不适用于:构造函数、原型函数、DOM事件函数等
- 普通函数:调用方式决定了
- 改变
thiscall():使用call方法调用函数,同时指定被调用函数中this的值
语法:
fun.call(thisArg, arg1, arg2, ...)thisArg:在fun函数运行时指定的this值args1, arg2:传递的其他参数- 返回值就是函数的返回值,因为它就是调用函数
apply():使用apply方法调用函数,同时制定被调用函数中的this的值
语法:
fun.apply(thisArg, [argsArray])thisArg:在fun函数运行时指定的this值argsArray:传递的值,必须包含在数组里面- 返回值就是函数的返回值,因为它就是调用函数
bind():不会调用函数,但是能改变函数内部this指向
语法:
fun.bind(thisArg, arg1, arg2, ...)thisArg:在fun函数运行时指定的this值arg1, arg2:传递的其他参数- 返回由指定的
this值和初始化参数改造的原函数拷贝(新函数) - 因此当我们只想改变
this指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this指向