JavaScript高级
基础总结深入
1.数据类型
1.1. 分类(2大类)
- 基本(值)类型
- Number: 任意数值
- String: 任意文本
- Boolean: true/false
- undefined: undefined
- null: null
- 对象(引用)类型
- Object: 任意对象
- Array: 特别的对象类型(下标/内部数据有序)
- Function: 特别的对象类型(可执行)
1.2. 判断
-
typeof:
- 返回的是数据类型的字符串表达形式
- 可以区别: 数值, 字符串, 布尔值, undefined, function
- 不能区别: null与对象, 一般对象与数组
-
instanceof
- 专门用来判断对象数据的类型: Object, Array与Function
- A instanceof B : 判断A是不是B的实例
-
===
- 可以判断: undefined和null
//1.基本数据类型
var a
console.log(a, typeof a, a===undefined) // undefined 'undefined' true
console.log(a===typeof a) // false
a = 3
console.log(typeof a === 'number') //true
a = 'atguigu'
console.log(typeof a === 'string') //true
a = true
console.log(typeof a === 'boolean') //true
a = null
console.log(a===null) // true
console.log(typeof a) // 'object'
//2.对象类型
var b1 = {
b2: [2, 'abc', console.log],
b3: function () {
console.log('b3()')
}
}
console.log(b1 instanceof Object, typeof b1) // true 'object'
console.log(b1.b2 instanceof Array, typeof b1.b2) // true 'object'
console.log(b1.b3 instanceof Function, typeof b1.b3) // true 'function'
console.log(typeof b1.b2[2]) // 'function'
console.log(b1.b2[2]('abc')) // 'abc' undefined
1.3. undefined与null的区别?
- undefined代表没有赋值
- null代表赋值了, 只是值为null
var a1
var a2 = null
console.log(a1, a2)
1.4. 什么时候给变量赋值为null呢?
var a = null //a将指向一个对象, 但对象此时还没有确定
a = null //让a指向的对象成为垃圾对象
//初始
var a3 = null
//中间
var name = 'Tom'
var age = 12
a3 = {
name: name,
age: age
}
//结束
a3 = null
1.5. 严格区别变量类型与数据类型?
- js的变量本身是没有类型的, 变量的类型实际上是变量内存中数据的类型
- 变量类型:
- 基本类型: 保存基本类型数据的变量
- 引用类型: 保存对象地址值的变量
- 数据对象
- 基本类型
- 对象类型
2.数据_变量_内存
2.1. 什么是数据?
- 存储于内存中代表特定信息的’东东’, 本质就是0101二进制
- 具有可读和可传递的基本特性
- 万物(一切)皆数据, 函数也是数据
- 程序中所有操作的目标: 数据
- 算术运算
- 逻辑运算
- 赋值
- 调用函数传参
…
2.2. 什么是内存?
- 内存条通电后产生的存储空间(临时的)
- 产生和死亡: 内存条(集成电路板)>通电>产生一定容量的存储空间==>存储各种数据==>断电==>内存全部消失
- 内存的空间是临时的, 而硬盘的空间是持久的
- 一块内存包含2个数据
- 内部存储的数据(一般数据/地址数据)
- 内存地址值数据
- 内存分类
- 栈: 全局变量, 局部变量 (空间较小)
- 堆: 对象 (空间较大)
2.3. 什么是变量?
- 值可以变化的量, 由变量名与变量值组成
- 一个变量对应一块小内存, 变量名用来查找到内存, 变量值就是内存中保存的内容
2.4. 内存,数据, 变量三者之间的关系
-
内存是一个容器, 用来存储程序运行需要操作的数据
-
变量是内存的标识, 我们通过变量找到对应的内存, 进而操作(读/写)内存中的数据
问题: var a = xxx, a内存中到底保存的是什么?
-
xxx是一个基本数据: a保存的就是这个数据
-
xxx是一个对象: a保存的就是这个地址值
-
xxx是一个变量: a保存的都是xxx的内容(可能是基本数据,也可能是地址值)
2.5.关于引用变量赋值问题
- 2个引用变量指向同一个对象, 通过一个引用变量修改对象内部数据, 另一个引用变量看到的是修改之后的数据
var obj1 = {name:'Jack'}
var obj2 = obj1
obj2.name = 'Tom'
console.log(obj1.name)//Tom
function f1(obj) {
obj.age = 12
}
f1(obj2)
console.log(obj1.age)//12
- 2个引用变量指向同一个对象,让一个引用变量指向另一个对象, 另一个引用变量还是指向原来的对象
var obj3 = {name: 'Tom'}
var obj4 = obj3
obj3 = {name: 'JACK'}
console.log(obj4.name)//Tom
function f2(obj) {
obj = {name: 'Bob'}
}
f2(obj4)
console.log(obj3.name)//JACK
console.log(obj4.name)//Tom
2.6.问题: 在js调用函数时传递变量参数时, 是值传递还是引用传递
- 只有值传递, 没有引用传递, 传递的都是变量的值, 只是这个值可能是基本数据, 也可能是地址(引用)数据
- 如果后一种看成是引用传递, 那就值传递和引用传递都可以有
2.7.JS引擎如何管理内存?
- 内存生命周期
1). 分配需要的内存
2). 使用分配到的内存
3). 不需要时将其释放/归还 - 释放内存
局部变量:函数执行完自动释放
对象:成为垃圾对象==>垃圾回收器回收
* 为执行函数分配的栈空间内存: 函数执行完自动释放
* 存储对象的堆空间内存: 当内存没有引用指向时, 对象成为垃圾对象, 垃圾回收器后面就会回收释放此内存
3.对象
3.1.什么是对象
- 代表现实中的某个事物, 是该事物在编程中的抽象
- 多个数据的集合体(封装体)
- 用于保存多个数据的容器
var obj = {
name:'Tom',
age:'12'
}
3.2. 为什么要用对象?
- 便于对多个数据进行统一管理
3.3. 对象的组成
- 属性
- 代表现实事物的状态数据
- 由属性名和属性值组成
- 属性名都是字符串类型, 属性值是任意类型
- 方法
- 代表现实事物的行为数据
- 是特别的属性==>属性值是函数
var obj = {
name:'Tom',
age:'12',
setName:function(name){
this.name=name;
},
setAge:function(age){
this.age=age;
}
}
3.4. 如何访问对象内部数据?
- .属性名: 编码简单, 但有时不能用
- [‘属性名’]: 编码麻烦, 但通用
console.log(p.name, p['age'])//Tom 12
p.setName('Jack')
p['setAge'](23)
console.log(p['name'], p.age)//Jack 23
问题: 什么时候必须使用[‘属性名’]的方式?
- 属性名不是合法的标识名
包含特殊字符: - 空格 - 属性名不确定
var p = {}
var propName = 'myAge'
var value = 18
//p.propName = value 不可用
p[propName] = value
console.log(p[propName])//18
4.函数
4.1. 什么是函数?
- 具有特定功能的n条语句的封装体
- 只有函数是可执行的, 其它类型的数据是不可执行的
- 函数也是对象
/*
编写程序实现以下功能需求:
1. 根据年龄输出对应的信息
2. 如果小于18, 输出: 未成年, 再等等!
3. 如果大于60, 输出: 算了吧!
4. 其它, 输出: 刚好!
*/
function show(age){
if(age<18){
console.log('未成年, 再等等!')
}else if(age>60){
console.log('算了吧!')
}else{
console.log('刚好!')
}
}
showInfo(10)
showInfo(22)
showInfo(88)
4.2. 为什么要用函数?
- 提高代码复用
- 便于阅读和交流
4.3. 如何定义函数?
- 函数声明
- 表达式
function fn1() {//声明式
console.log("fn1()")
}
var fn2 = function () {//表达式
console.log("fn2()")
}
fn1()
fn2()
4.4. 如何调用(执行)函数?
- test()
- new test()
- obj.test()
- test.call/apply(obj):临时让test成为obj的方法进行调用
var obj = {}
function test2 (){
this.xxx = 'atiguigu'
}
//obj.test2 不能直接调用,因为obj属性没有这个方法
test2.call(obj) //可以让一个函数成为指定任意对象的方法进行调用
console.log(obj.xxx)
5.回调函数
5.1. 什么函数才是回调函数?
- 你定义的
- 你没有直接调用
- 但最终它执行了(在特定条件或时刻)
5.2. 常见的回调函数?
-
DOM事件函数
-
定时器函数
-
ajax回调函数(后面学)
-
生命周期回调函数(后面学)
//1. DOM事件函数
var btn = document.getElementById('btn')
btn.onclick = function () {
alert(this.innerHTML)
}
//2. 定时器函数
setInterval(function () {
alert('到点啦!')
}, 2000)
6.IIFE
6.1. 理解
- 全称: Immediately-Invoked Function Expression 立即调用函数表达式
- 别名: 匿名函数自调用
(function (){
console.log("……")
})()
6.2. 作用
- 隐藏内部实现
- 不污染外部命名空间
(function (i) {
var a = 4
function fn() {
console.log('fn ', i+a)
}
fn()
})(3)//fn 7
7.函数中的this
7.1. this是什么?
- 所有函数内部都有一个变量this
- 一个关键字, 一个内置的引用变量
- 在函数中都可以直接使用this
- this代表调用函数的当前对象
- 在定义函数时, this还没有确定, 只有在执行时才动态确定(绑定)的
7.2. 如何确定this的值?
- test():window
- obj.test():obj
- new test():新创建的对象
- test.call(obj):obj
前置知识: - 本质上任何函数在执行时都是通过某个对象调用的
function Person(color) {
console.log(this)
this.color = color;
this.getColor = function () {
console.log(this)
return this.color;
};
this.setColor = function (color) {
console.log(this)
this.color = color;
};
}
Person("red"); //this是谁?window
var p = new Person("yello"); //this是谁?p
p.getColor(); //this是谁?p
var obj = {};
p.setColor.call(obj, "black"); //this是谁?obj
var test = p.setColor;
test(); //this是谁?window
function fun1() {
function fun2() {
console.log(this);
}
fun2(); //this是谁?window
}
fun1();
函数高级
1.原型与原型链
1.1.原型(prototype)
1.1.1. 函数的prototype属性(图)
- 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
- 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype, typeof Date.prototype)
function fn() {
}
console.log(fn.prototype, typeof fn.prototype)
- 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor===Date)//true
console.log(fn.prototype.constructor===fn)//true
1.1.2. 给原型对象添加属性(一般都是方法)
- 作用: 函数的所有实例对象自动拥有原型中的属性(方法)
function F() {
}
F.prototype.age = 12 //添加属性
F.prototype.setAge = function (age) { // 添加方法
this.age = age
}
// 创建函数的实例对象
var f = new F()
console.log(f.age)//12
f.setAge(23)
console.log(f.age)//23
1.2.显式原型与隐式原型
1.2.1. 每个函数function都有一个prototype,即显式原型
1.2.2. 每个实例对象都有一个__proto__,可称为隐式原型
function Fn() {
}
console.log(Fn.prototype)//1.显式
var fn = new Fn()//内部语句:this.__proto__=Fn.prototype
console.log(fn.__proto__)//2.隐式
1.2.3. 对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(fn.__proto__===Fn.prototype)//true
1.2.4. 内存结构(图)
增加属性
1.2.5. 总结:
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
- 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
1.3.原型链
1.3.1. 原型链(图解)
- 访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着__proto__这条链向上查找, 找到返回
- 如果最终没找到, 返回undefined
- 别名: 隐式原型链
- 作用: 查找对象的属性(方法)
function Fn() {
this.test1 = function () {
console.log('test1()')
}
}
Fn.prototype.test2 = function () {
console.log('test2()')
}
var fn = new Fn()
fn.test1()//test1()
fn.test2()//test2()
console.log(fn.toString())//[object,object]
fn.test3()//报错
1.3.2 原型链特点
原型链的特点主要有两个:
- 由于原型链的存在,属性查找的过程不再是只查找自身的原型对象,而是会沿着整个原型链一直向上,直到追溯到Object.prototype.如果Object.prototype上也找不到该属性,则返回undefined,如果期间在实例本身或者某个原型对象上找到了该属性,会直接返回结果,因此也会存在属性覆盖的问题。
由于特点1的存在,我们在生成定义对象的实例时,也可以调用到某些未调用到某些未在定义构造函数上的函数,例如toString()方法
2.由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
1.3.3.属性区分
对象属性的寻找往往会涉及这个原型链,那么该怎么区分属性是实例自身还是从原型链中继承的呢?
Object()构造函数的原型对象中提供了一个**hasOwnProperty()**函数,用于判断是否为自身拥有的。
function Person(name) {
//实例属性name
this.name = name;
}
Person.prototype.age = '18';
var person = new Person('name');
console.log(person.hasOwnProperty('name'));//true
console.log(person.hasOwnProperty('age'));//false
1.3.4.内置构造函数
js内部有一些特定的内置构造函数,他们本身的__proto__属性都统一指向Function.prototype
console.log(String.__proto__ === Function.prototype);
console.log(Number.__proto__ === Function.prototype);
console.log(Array.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Date.__proto__ === Function.prototype);
console.log(Function.__proto__ === Function.prototype);
//全为true
1.3.5.__proto__属性
在js的原型链体系中,最重要的莫过于__proto__属性,只有通过它才能将原型链串联起来。
var str = new String('linjuejie');
console.log(str);
所以我们就能通过str来调用对应的toString()方法,trim()方法等。
Function.prototype.a = 'a';
Object.prototype.b = 'b';
function Person() {
};
var p = new Person();
console.log('p.a:',p.a);//undefined
console.log('p.b:',p.b);//b
1.4.探索instanceof
1.4.1.instanceOf是怎么判断的
表达式:A instanceOf B
如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false
function Foo() {
};
var f1 = new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object);//true
console.log(Object instanceof Function);//true
console.log(Object instanceof Object);//true
console.log(Function instanceof Object);//true
console.log(Function instanceof Function);//true
function foo() {
};
console.log(Object instanceof foo);//false
1.5.常见面试题
var A = function() {
}
A.prototype.n = 1
var b = new A()
console.log(b.__proto__)
A.prototype = {
n: 2,
m: 3
}
var c = new A()
console.log(b.n, b.m, c.n, c.m)//1 undefined 2 3
var F = function(){};
Object.prototype.a = function(){
console.log('a()')
};
Function.prototype.b = function(){
console.log('b()')
};
var f = new F();
f.a()
// f.b()报错
F.a()
F.b()
2.执行上下文与执行上下文栈
2.1.变量提升与函数提升
2.1.1. 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
var a = 4
function fn () {
console.log(a)
var a = 5
}
fn()//undefined
/*变量提升*/
console.log(a1) //可以访问, 但值是undefined
var a1 = 3
2.1.2. 函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
/*函数提升*/
a2() // 可以直接调用
function a2() {
console.log('a2()')
}
2.1.3. 问题: 变量提升和函数提升是如何产生的?
前置知识:执行上下文
- 全局执行上下文:(全局代码执行时JS所做的前置准备工作)
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量赋值为undefined,并且添加为window的属性;
- function申明的全局函数复制(fun),添加为window的方法;
- this赋值window;
- 开始执行全局代码
所以全局代码并不是像我们书写的顺序而执行,而是通过系统的预处理更改了代码顺序后执行。
2.2.执行上下文
2.2.1. 代码分类(位置)
- 全局代码
- 函数代码
2.2.2. 全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
console.log(a1,window.a1)//undefined undefined
console.log(a2)//undefined
console.log(a3)
// console.log(a4)
console.log(this)
var a1 = 3
var a2 = function () {
console.log('a2()')
}
function a3() {
console.log('a3()')
}
a4 = 4
2.2.3. 函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的,存在于栈中)
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
function fn(x, y) {
console.log(x, y)
console.log(b1)
console.log(b2)
console.log(arguments)
console.log(this)
// console.log(b3)
var b1 = 5
function b2 () {
}
b3 = 6
}
fn()
2.3.执行上下文栈
2.3.1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2.3.2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
2.3.3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
2.3.4. 在当前函数执行完后,将栈顶的对象移除(出栈)
2.3.5. 当所有的代码执行完后, 栈中只剩下window
//1. 进入全局执行上下文
var a = 10
var bar = function (x) {
var b = 5
foo(x + b) //3. 进入foo执行上下文
}
var foo = function (y) {
var c = 5
console.log(a + c + y)
}
bar(10) //2. 进入bar函数执行上下文
3.作用域与作用域链
3.1.作用域
3.1.1. 理解
- 就是一块"地盘", 一个代码段所在的区域
- 它是静态的(相对于上下文对象), 在编写代码时就确定了
3.1.2. 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
3.1.3. 作用
- 隔离变量,不同作用域下同名变量不会有冲突
if(true) {
var c = 3
}
console.log(c)//3
var a = 10,
b = 20
function fn(x) {
var a = 100,
c = 300;
console.log('fn()', a, b, c, x)//fn() 100 20 300 10
function bar(x) {
var a = 1000,
d = 400
console.log('bar()', a, b, c, d, x)
}
bar(100)//bar() 1000 20 300 400 100
bar(200)//bar() 1000 20 300 400 200
}
fn(10)
3.2.作用域与执行上下文
3.2.1. 区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
3.2.2. 区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
3.2.3. 联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
3.3.作用域链
3.3.1. 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
3.3.2. 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
var a = 1
function fn1() {
var b = 2
function fn2() {
var c = 3
console.log(c)
console.log(b)
console.log(a)
console.log(d)
}
fn2()
}
fn1()
3.4.常见面试题
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn);//10
x的作用域在定义时就已经确定
var fn = function () {
console.log(fn)
}
fn()//fn的方法
var obj = {
fn2: function () {
console.log(fn2)//报错
//console.log(this.fn2)
}
}
obj.fn2()
4.闭包
4.1.理解闭包
4.1.1. 如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
4.1.2. 闭包到底是什么?
- 使用chrome调试查看
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量(函数)的对象(极少数人)
- 注意: 闭包存在于嵌套的内部函数中
4.1.3. 产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
function fn1 () {
var a = 2
var b = 'abc'
function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)
console.log(a)
}
}
fn1()
//fn2()的闭包中包含a但不包含b,执行函数定义就会产生闭包(不需要调用函数)
4.2.常见的闭包
4.2.1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
4.2.2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
4.3.闭包的作用
4.3.1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
4.3.2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
4.4.闭包的生命周期
4.4.1.产生
在嵌套内部函数定义执行完时就产生了
4.4.2.死亡
在嵌套的内部函数成为垃圾对象时
4.5.闭包的应用
定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
//myModule.js
function myModule() {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//如果只写到这里,调用函数之后会消亡
//向外暴露对象(给外部使用的方法)
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
//html页面
var module = myModule()
module.doSomething()
module.doOtherthing()
}
(function () {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})()
//html
myModule2.doSomething()
myModule2.doOtherthing()
4.6.闭包的缺点及解决
4.6.1. 缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
4.6.2. 解决
- 能不用闭包就不用
- 及时释放
function fn1() {
var arr = new Array[100000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象-->回收闭包
4.7.常见面试题
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //? the window
//代码片段二
var name2 = "The Window";
var object2 = {
name2 : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name2;
};
}
};
alert(object2.getNameFunc()()); //? my object
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0)//fun(0,undefined)
a.fun(1)//return fun(1,0)
a.fun(2)
a.fun(3)//undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3)//undefined,0,1,2
//.fun(2):fun(1,0).fun(2) return fun(2,1)
var c = fun(0).fun(1)//fun(1,0)
c.fun(2)//
c.fun(3)//undefined,0,1,1
对象高级
1.对象创建模式
1.1.Object构造函数模式
- 套路: 先创建空Object对象, 再动态添加属性/方法
- 适用场景: 起始时不确定对象内部数据
- 问题: 语句太多
// 先创建空Object对象
var p = new Object()
p = {} //此时内部数据是不确定的
// 再动态添加属性/方法
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
this.name = name
}
//测试
console.log(p.name, p.age)
p.setName('Bob')
console.log(p.name, p.age)
1.2对象字面量
- 套路: 使用{}创建对象, 同时指定属性/方法
- 适用场景: 起始时对象内部数据是确定的
- 问题: 如果创建多个对象, 有重复代码
var p = {
name: 'Tom',
age: 12,
setName: function (name) {
this.name = name
}
}
//测试
console.log(p.name, p.age)
p.setName('JACK')
console.log(p.name, p.age)
var p2 = { //如果创建多个对象代码很重复
name: 'Bob',
age: 13,
setName: function (name) {
this.name = name
}
1.3.工厂模式
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题: 对象没有一个具体的类型, 都是Object类型
function createPerson(name, age) { //返回一个对象的函数===>工厂函数
var obj = {
name: name,
age: age,
setName: function (name) {
this.name = name
}
}
return obj
}
// 创建2个人
var p1 = createPerson('Tom', 12)
var p2 = createPerson('Bob', 13)
// p1/p2是Object类型
function createStudent(name, price) {
var obj = {
name: name,
price: price
}
return obj
}
var s = createStudent('张三', 12000)
// s也是Object
1.4.自定义构造函数模式
- 套路: 自定义构造函数, 通过new创建对象
- 适用场景: 需要创建多个类型确定的对象
- 问题: 每个对象都有相同的数据, 浪费内存
function Person(name, age) {
this.name = name
this.age = age
this.setName = function (name) {
this.name = name
}
}
var p1 = new Person('Tom', 12)
p1.setName('Jack')
console.log(p1.name, p1.age)
console.log(p1 instanceof Person)
function Student (name, price) {
this.name = name
this.price = price
}
var s = new Student('Bob', 13000)
console.log(s instanceof Student)
var p2 = new Person('JACK', 23)
console.log(p1, p2)
1.5.构造函数+原型的组合模式
- 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要创建多个类型确定的对象
function Person(name, age) { //在构造函数中只初始化一般函数
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 23)
var p2 = new Person('Jack', 24)
console.log(p1, p2)//方法在原型上,是同一个方法
2.继承模式
2.1.原型链继承
继承作为面向对象语言的三大特性之一,三大特性是(封装,继承,多态),可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性,同时还能在不影响父类对象行为的情况下拓展子类独有的属性,为编码带来了极大的便利。
虽然JavaScript不是一门面向对象的语言,不具备继承的特性,但是我们可以通过某些方式间接实现继承,从而能利用继承的优势,增强代码复用性与拓展性。
function Animal(name) {
this.type = 'animal';
this.name = name || '动物';
this.sleep = function () {
console.log(this.name + '正在睡觉!');
}
}
//原型链上的属性和方法
Animal.prototype.age = 5;
Animal.prototype.eat = function (food) {
console.log(this.name + "正在吃" + food);
}
// 子类
function Cat(name) {
this.name = name;
}
//原型链继承
Cat.prototype = new Animal();
//很关键的一句,将Cat的构造函数指向自身
var cat = new Cat('加菲猫');
console.log(cat.type);//animal
console.log(cat.name);//加菲猫
cat.sleep();//加菲猫正在睡觉!
console.log(cat.age);//5
cat.eat('猫粮');//加菲猫正在吃猫粮
var animal = new Animal();
console.log(animal.type);//animal
console.log(animal.name);//动物
animal.sleep();//动物正在睡觉!
console.log(animal.age);//5
animal.eat('肉类');//动物正在吃肉类
优点
- 简单,容易实现
只需要设置子类的prototype属性为父类的实例即可,实现起来简单。 - 继承关系纯粹
生成的实例既是子类的实例,也是父类的实例 - 可以通过子类直接访问父类原型链属性和函数
通过原型链继承的子类,可以直接访问到父类原型链上新增的函数和属性
缺点
- 子类的所有实例将共享父类的属性
在使用原型链继承时,如果父类有个属性的值为引用类型,那么改变Cat某个实例的属性值将会影响其他实例的属性值 - 在创建子类实现时,无法向父类的构造函数传递参数
在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类的关联,从而导致无法向父类的构造函数传递参数 - 无法实现多继承
由于子类prototype的属性只能设置为一个值,如果同时设置为多个值的话,后面的值会覆盖前面的值,导致Cat只能继承一个父类,而无法实现多继承 - 为子类增加原型对象上的属性和函数时,必须放在new Animal()后
实现继承的关键语句是下面这句代码,它实现了子类prototype属性的改写。
Cat.prototype = new Animal();
如果想要为子类新增原型对象上的属性和函数,那么需要在这个语句之后添加。因为如果在这个语句之前设置了prototype属性,后面的语句会直接重写prototype属性,导致之前的设置全部失效
2.2. 借用构造函数继承(假的)
- 套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
- 关键:
- 在子类型构造函数中通用call()调用父类型构造函数
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s = new Student('Tom', 20, 14000)
console.log(s.name, s.age, s.price)
2.3.组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用call()借用父类型构建函数初始化相同属性
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
function Student(name, age, price) {
Person.call(this, name, age) // 为了得到属性
this.price = price
}
Student.prototype = new Person() // 为了能看到父类型的方法
Student.prototype.constructor = Student //修正constructor属性
Student.prototype.setPrice = function (price) {
this.price = price
}
var s = new Student('Tom', 24, 15000)
s.setName('Bob')
s.setPrice(16000)
console.log(s.name, s.age, s.price)
线程机制与事件机制
1.进程与线程
- 进程:程序的一次执行, 它占有一片独有的内存空间
- 线程: CPU的基本调度单位, 是程序执行的一个完整流程
是进程内的一个独立执行单元
是程序执行的一个完整流程
是CPU的最小的调度单元 - 进程与线程
- 一个进程中一般至少有一个运行的线程: 主线程
- 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 浏览器运行是单进程还是多进程?
- 有的是单进程
- firefox
- 老版IE
- 有的是多进程
- chrome
- 新版IE
- 如何查看浏览器是否是多进程运行的呢?
- 任务管理器==>进程
- 浏览器运行是单线程还是多线程?
- 都是多线程运行的
2.浏览器内核
- 什么是浏览器内核?
- 支持浏览器运行的最核心的程序
- 不同的浏览器可能不太一样
- Chrome, Safari: webkit
- firefox: Gecko
- IE: Trident
- 360,搜狗等国内浏览器: Trident + webkit
- 内核由很多模块组成
-
html,css文档解析模块 : 负责页面文本的解析
-
dom/css模块 : 负责dom/css在内存中的相关处理
-
布局和渲染模块 : 负责页面的布局和效果的绘制
-
布局和渲染模块 : 负责页面的布局和效果的绘制
-
定时器模块 : 负责定时器的管理
-
网络请求模块 : 负责服务器请求(常规/Ajax)
-
事件响应模块 : 负责事件的管理
JS是单线程还是多线程?
js是单线程运行的,但使用H5中的Web Workers可以多线程运行
3.定时器引发的思考
- 定时器真是定时执行的吗?
- 定时器并不能保证真正定时执行
- 一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)
- 定时器回调函数是在分线程执行的吗?
- 在主线程执行的, js是单线程的
- 定时器是如何实现的?
- 事件循环模型(后面讲)
document.getElementById('btn').onclick = function () {
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function () {
console.log('定时器执行了', Date.now()-start)
}, 200)
console.log('启动定时器后...')
// 做一个长时间的工作
for (var i = 0; i < 1000000000; i++) {
}
}
4.JS是单线程的
- 如何证明js执行是单线程的?
- setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
- 为什么js要用单线程模式, 而不用多线程模式?
- JavaScript的单线程,与它的用途有关。
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
- 这决定了它只能是单线程,否则会带来很复杂的同步问题
- 代码的分类:
- 初始化代码
- 回调代码
- js引擎执行代码的基本流程
- 先执行初始化代码: 包含一些特别的代码 回调函数(异步执行)
- 设置定时器
- 绑定事件监听
- 发送ajax请求
- 后面在某个时刻才会执行回调代码
setTimeout(function () {
console.log('timeout 2222')
alert('22222222')
}, 2000)
setTimeout(function () {
console.log('timeout 1111')
alert('1111111')
}, 1000)
setTimeout(function () {
console.log('timeout() 00000')
}, 0)
function fn() {
console.log('fn()')
}
fn()
console.log('alert()之前')
alert('------') //暂停当前主线程的执行, 同时暂停计时, 点击确定后, 恢复程序执行和计时
console.log('alert()之后')
5.事件循环模型
- 所有代码分类
- 初始化执行代码(同步代码): 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
- 回调执行代码(异步代码): 处理回调逻辑
- js引擎执行代码的基本流程:
- 初始化代码===>回调代码
- 模型的2个重要组成部分:
- 事件(定时器/DOM事件/Ajax)管理模块
- 回调队列
- 模型的运转流程
- 执行初始化代码, 将事件回调函数交给对应模块管理
- 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
- 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行
6.H5 web Workers
- H5规范提供了js分线程的实现, 取名为: Web Workers
- 相关API
- Worker: 构造函数, 加载分线程执行的js文件
- Worker.prototype.onmessage: 用于接收另一个线程的回调函数
- Worker.prototype.postMessage: 向另一个线程发送消息
- 不足
- worker内代码不能操作DOM(更新UI)
- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
//创建一个Worker对象
var worker = new Worker('worker.js')
// 绑定接收消息的监听
worker.onmessage = function (event) {
console.log('主线程接收分线程返回的数据: '+event.data)
alert(event.data)
}
// 向分线程发送消息
worker.postMessage(number)
console.log('主线程向分线程发送数据: '+number)
}
// console.log(this) // window
</script>
</body>
//worker.js
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
console.log(this)
this.onmessage = function (event) {
var number = event.data
console.log('分线程接收到主线程发送的数据: '+number)
//计算
var result = fibonacci(number)
postMessage(result)
console.log('分线程向主线程返回数据: '+result)
// alert(result) alert是window的方法, 在分线程不能调用
// 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面
}
补充
1.分号问题
- js一条语句的后面可以不加分号
- 是否加分号是编码风格问题, 没有应该不应该,只有你自己喜欢不喜欢
- 在下面2种情况下不加分号会有问题
- 小括号开头的前一条语句
- 中方括号开头的前一条语句
- 解决办法: 在行首加分号
- 强有力的例子: vue.js库
- 知乎热议: https://www.zhihu.com/question/20298345
// 情形一: 小括号开头的前一条语句
var a = 3
;(function () {
})
/*
错误理解: 将3看成是函数调用
var a = 3(function () {
})
*/
// 情形二: 中方括号开头的前一条语句
var b = a
;[1, 3, 5].forEach(function (item) {
console.log(item)
})
/*
错误理解:
a = b[5].forEach(function(e){
console.log(e)
})
*/
2.内存溢出与内存泄漏
2.1. 内存溢出
- 一种程序运行出现的错误
- 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
2.2. 内存泄露
- 占用的内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
- 常见的内存泄露:
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包