- 1. 底层知识点
- 2. 变量提升
- 8. 堆栈内存
- 9. 函数的存储方式 及执行过程
- 10. 闭包
- 11. 内存优化
- 12. let的块级作用域与闭包应用
- 14. 函数执行双私有上下文
- 15. 闭包作用域
- 16. 高阶编程思想
- 17. let const var 区别
- 18. 闭包作用域练习题
- 19. jQuery 源码思想
- 20. This指向
- 21. 面向对象编程(OOP)
- 22. 构造函数执行的细节与知识点
- 23. 原型/原型链
- 25. new方法重写
- 26. 内置类原型扩展注意点
- 29. Function与Object之间的关系
- 32.函数的三种角色
- 33. 多种继承方式
- 37. instanceof 重写
- 38. 原型上扩展方法事项
- 39. 面向对象的变相应用
- 41. bind 重写
- 44. 闭包, 面向对象 综合
声明: 个人学习使用,全文手敲, 由于语雀性能问题, 拆分上下文档
1. 底层知识点
1. 数据类型
基本类型在内存中保存key 跟value
引用类型在内存中保存一个地址 AAABBBFFF 然后这个地址指向创建的变量
序列 | 基本类型: | 引用类型: |
---|---|---|
1 | Number | Object |
2 | String | Array |
3 | Boolean | Function |
4 | Null | |
5 | Undefined | |
6 | Symbol | |
7 | BigInt |
对象的属性名一定不是引用类型值,默认转为字符串
对象的toString为[object object]
2. 引擎 v8 webkit内核
GO VO AO EC ECStack
- 堆栈内存,
- 堆:存放引用类型, 函数等。。
- 栈: 执行上下文EC 存储基本类型
- GO global object 在浏览器端,会把全局对象赋值给window
- VO 变量对象 存储当前上下文中的变量
- AO 活动的变量对象
- JS引擎 想要执行代码,一定会创建执行栈, 然后进栈,执行完一般情况会出栈(存在闭包不会)
3. 变量赋值分为三步 声明及存储
1.创建变量 如果创建不用再次创建
2.创建值:基本值直接在栈中创建
3.让变量与值关联起来 定义(defined);
由于引用类型的值是一个复杂的结构,所以特殊处理 ,
专门为它开辟一个存储对象中的键值对(存储函数中的代码)的内存空间
成为堆内存
所有的堆内存都有一个可被后续查找的16进制地址比如AAAFFF111
后续关联赋值的时候是把堆内存的地址赋值给变量操作
把一个变量赋值为null 空对象指针
let a = {
n: 10
}
let b = a;
b.m = b = {
n: 20
}
var c = {
n: 20,
m: {
n: 20
}
}
console.log(a);
console.log(b);
代码执行全过程
ECStack执行环境栈
全局执行上下文 进栈执行
浏览器window会指向GO global
创建变量,
创建值 如果基本类型直接在栈内创建 引用类型在开辟一个堆内存
赋值 把值关联给变量 指针指向变量
2. 变量提升
ES3/5规范
- 判断体合函数体不存在块级上下文, 上下文只有全局和私有
-
ES6规范
存在快级作用域, 大括号{ … } 中出现 let/const/function 都会被认为是块级作用域
- 不论条件是否成立, 带function的只提前声明 ,但不会提前赋值
因为要兼容ES3/6 function如果在全局下声明过, 也在私有下/块级上下文处理过, 会把声明的值映射给全局一份数据, 以此兼容ES3, 但是后面的代码就与全局没有任何关系
练习题
var a = 12;
if (true) {
/*
* 块级上下文
* function a() {};
*/
console.log(a); //=>函数
a = 13;
console.log(a); //=>13
function a() {} //之前对a的操作映射给全局一份 全局a=13
a = 14;
console.log(a); //=>14
}
console.log(a); //=>13
8. 堆栈内存
创建变量且赋值操作
var a = 1 // 做了以下操作
创建一个值
- 基本类型值直接存储在栈内存中
- 引用数据类型的值需要先开辟一个堆内存, 把内存存储进去后把堆内存地址存放到栈,提供变量使用
- 创建一个变量(声明:declare) 存储在变量对象中(VO)
让变量和值进行关联 (指针指向的过程) (定义: defined)
对象的存储
对象数据类型:由零到多组键值对(属性名和属性值)组成的
- 属性名的类型:
【说法一:属性名类型只能是字符串或者Symbol】
【说法二:属性名类型可以是任何基本类型值,处理中可以和字符串互通】
但是属性名绝对不能是引用数据类型,如果设置引用类型,最后也是转换为字符串处理的
let sy = Symbol('AA');
let x = {
0: 0
};
let obj = {
0: 12,
true: 'xxx',
null: 20
};
obj[sy] = '珠峰';
obj[x] = 100; //=> obj['[object Object]']=100 会把对象变为字符串作为属性名
for (let key in obj) {
// FOR IN遍历中获取的属性名都会变为字符串
// 并且无法迭代到属性名是SYMBOL类型的属性
console.log(key, typeof key);
}
连等赋值
- 运算符优先级相等时 从右到左
- 运算符优先级查看链接
- 优先级大的先执行 ```javascript // 从右到左 // 1. 创建一个值12 // 2. b=12 // 3. let a=12 let a = b = 12;
a.x = a = {}; // 相当于a = a.x = {}; // 因为成员访问 a.x 的优先级是很大的,所以不论怎么调换位置,都是先处理a.x={} */
```javascript
// 练习题
var a = { n: 1 }
var b = a
a.x = a = { n: 2 }
console.log(a.x) // undefined
console.log(b) // {n: 1, x: { n: 2 }}
图解: 对象连续赋值.png
常用运算符优先级
优先级相同, 从右往左执行
分组 | () | 21 |
---|---|---|
成员访问 | obj. | 20 |
带参数new() | new(*) | |
函数调用 | fn() | |
可选链 | ?. | |
不带参数new | new xx | 19 |
逻辑与 | && | 7 |
逻辑或 | || | 6 |
9. 函数的存储方式 及执行过程
- EC(stack) 执行环境
- 执行上下文, (全局EC(G)/私有)
- 执行上下文进栈运行, 执行完还可能出栈
函数创建
- 开辟一个堆内存: 16地址 AAAFFF000
- 声明当前函数的作用域, 在哪上下文创建的,他的作用域就是谁
- 函数体中代码当做字符串存储在堆内存中(创建一个函数, 存储的字符串, 如果不执行, 没有意义)
- 把函数的堆内存地址,提供函数名使用
堆内存存放返回地址指向
AAAFFF111 —转换为代码字符串
存储键值对对象
length 形参的个数
name 函数的名称
prototype 函数的原型
函数执行
非严格模式下,会创建映射机制,(参数相互关联)严格模式下不会创建映射机制 ,
箭头函数没有arguments
- 会形成一个全新的私有上下文EC(xx) ()目的是供函数体中的代码执行),然后进栈执行
- 在私有上下文中有一个存放私有变量的变量对象AO(xx)
- 在代码执行之前要做的事情很多:
- 初始化它的作用域链<自己上的上下文,函数的作用域>
- 初始化THIS(箭头函数没有THIS)
- 初始化ARGUMENTS实参类数组集合(箭头函数没有ARGUMENTS)(非严格模式会跟形参建立连接关系)
- 形参赋值(形参变量是函数的私有变量,需要存储在AO中的)
- 变量提升(在私有上下文中声明的变量都是私有变量)
- 代码执行: 把之前在函数堆中存储的代码字符串, 执行
- 作用域链查找变量: 查找机制 => 在代码中, 遇到一个变量 ,首先查看是否为自己私有变量, 如果没有, 则会按照scope-chain 向上级上下文查找, 一直找, 直到找到EC(G) 为止, 如果没有则会返回undefined, 在哪上下文找到, 接下来所有操作都是操作对应上下文的变量
- 根据实际的情况确定当前上下文是否出栈释放
- 普通函数执行完会释放
- 只要当前上下文的某些内容, 被上下文以外的东西占用, 那么当前上下文不会释放
函数二次执行,会形成一个全新的私有上下文,把之前做过的事情,还是原封不动的再执行一次(所有的东西都是从头来一遍的),此时形成的上下文和上一次形成的上下文之间没有必然的联系
图解: 函数执行图解.png
练习题: 练习闭包作用域.js
//形参的值假如是引用类型,形参都是私有的AO对象里面
let x = {n:100}; //假如实参是对象,指向地址
~function(x){
x.n =200;
console.log(x); //200
}(x)
console.log(x); //200
let x = 100;
~function(x){ //假如实参是基本类型 返回值,改变不会影响传入的源值
x =200;
console.log(x); //100
}(x)
console.log(x); //200
console.log(x); //200
- let 创建的变量一定是全局变量,只不过不是全局属性
- 函数也是一个变量 和let var 创建的变量本质是一样的,
- function fn(){} 正常的创建函数
- let fn = function(){} 函数表达式创建函数
- (function(){})() 匿名自掉函数
10. 闭包
机制: 形成一个不被释放的上下文就会产生闭包
闭包: 它是函数运行时所产生的一个机制, 函数执行会产生一个全新的私有上下文,可以保护里面的私有变量与外界互补干扰,
产生: 函数当前上下文的成员被外部作用域链牵引着就行程闭包(基本类型不会产生闭包)
优点: 保护/保存
缺点: 占用堆内存
闭包的应用
- 阐述ECStack/EC/AO/VO/Scope/SCOPE-CHAIN/释放不释放, 垃圾回收
- 实战用途
- 高阶编程: 柯理化/ 惰性函数/ compose函数
- 源码分析
- 自己封装插件组件库
11. 内存优化
- 栈内存: 执行上下文
- 一般情况, 函数执行完,所形成的上下文会被出栈释放
- 特殊情况, 当前上下文中的某些内容被当前上下文以外的事物占用了, 此时不能出栈释放
- 全局上下文, 加载页面时创建, 也只是页面关闭才会被释放掉
- 堆内存: 浏览器的垃圾回收机制
- 引用计数: (以IE为主) 使用一次计数一次, 释放一次减少一次, 计数为0时释放堆内存
- 检测引用/标记清除 (以chrome为主) 浏览器再空闲的时候会依次检测所有的堆内存, 把没有被占用的内存释放, 以此来优化内存
- 手动释放内存,其实就是解除占用(手动赋值为null即可)
12. let的块级作用域与闭包应用
事件委托
循环添加事件
// 最好的方法是事件委托(性能是最好的)
var arr=['red','green','blue','black','pink'];
document.body.onclick=function(ev){
let target=ev.target,targetTag=target.tagName;
//当前点击的是五个按钮中的一个,target事件源就是点击的这个按钮
if(targetTag ==="BUTTON"){
var index=target.getAttribute('index');document.body.style.backgroundColor=arr[index];
}
}
var与没有var区别
链式查找: 在当前上下文遇到一个变量,如果不是私有的,则向上级上下文中查找…一直找到EC(G)为止,如果EC(G)中也没有:
- 如果是获取变量的 值,则直接报错
- 如果是设置变量的值,则相当于给window(G0)设置一个属性
GO全局对象 和 VO(G)全局变量对象 的关系
1. 两者之间存在映射关系(创建一个全局变量,也相当于给GO设置一个属性)【刨除基于let/const创建的变量】
// 此时这个a只是GO的属性,相当于省略了window.,但是它不能理解为全局变量
a = 12;
console.log(a); // 12
console.log(window.a); // 12
练习题: 202020年04期在线JS高级(第一周作业
14. 函数执行双私有上下文
- Block(块级上下文)
- Local(私有上下文)
需满足条件: 函数执行就有两个上下文
- 有形参赋值默认值
- 函数体中有声明过自己的私有变量(必须是var/ let / const)声明 , function只有声明的名字和形参中的名字相同,才会单独产生块级上下文
练习题:
// 题目1
var x = 1;
function func(x, y = function anonymous1() {x = 2}) {
x = 3;
y();
console.log(x); //=>2
}
func(5);
console.log(x); //=>1
// 题目2
var x = 1;
function func(x, y = function anonymous1() {x = 2}) {
var x = 3; //把EC(BLOCK)中的x改为3
y(); //EC(BLOCK)没有私有变量y,所以找EC(FUNC)中的y执行
console.log(x); //=>输出的是EC(BLOCK)中的x 3
}
func(5);
console.log(x); //=>1
// 题目3
var x = 1;
function func(x, y = function anonymous1() {x = 2}) {
var x = 3;
var y = function anonymous2() {x = 4};
y();
console.log(x);
}
func(5);
console.log(x);
15. 闭包作用域
arguments相关
知识点:
- 函数执行预先初始化ARGUMENTS, 值为传递参数的类数组集合,
- 不管有没有形参都会初始化(箭头函数没有),
- 非严格模式下形参赋值完成, 会和ARGUMENTS中的每一项建立映射机制, 一个改另外一个也会跟着改, 但是在严格模式下不会(‘use strict’)
- 形参与arguments是在函数体执行之前建立连接, 如果没有建立连接, 则不会映射: 参考题目2
练习题:
// 题目1
var a = 4;
function b(x, y, a) {
console.log(a); // =>3
arguments[2] = 10;
console.log(a); // =>10
x=20;
console.log(arguments[0]); // =>20
}
a = b(1, 2, 3); // a=undefined 因为b函数执行没有返回值(主要看return)
console.log(a); // =>undefined
// 题目2
var a = 4;
function b(x, y, a) {
a = 2
arguments[2] = 10;
console.log(arguments[2]); // undefined
}
a = b(1, 2); // 传递2个参数
匿名函数具名化
知识点:
- 匿名函数具名化只能在函数内部使用, 外部无法使用,
- 在函数内部默认是不允许修改具名的值, 除非这个函数体重被重新声明过(function/var/let/const), 如果重新声明后, 名称的值会按照声明的为主
练习题:
// 要点1
var b = 10
(function b(){
b = 20
console.log(b) // 函数本身
})()
console.log(b) // 10
// 要点2
var b = 10
(function b(){
var b = 20
console.log(b) // 20
})()
console.log(b) // 10
16. 高阶编程思想
1.单例设计模式
单例设计模式: 用单独的实例来管理当前事务的相关特征, 类似于实现一个分组的特点
个人理解: 闭包包起来,返回方法
//不能使用~function(){}(),只能使用下面的方式,才有返回值
let Module = (function(){
let addFun = function(){
//操作....
}
return {
addFun
}
})()
//别人使用的时候可以采用Module.addFun
2.惰性函数 有些判断只做一次判断
//惰性函数思想 利用闭包减少判断的次数
function emit(element,type,func){
//兼容性判断只判断一次
//如果支持addEventListener 全局emit重写方法
if(element.addEventListener){
emit = function(element,type,func){
element.addEventListener(element,type,func)
};
}
else if(element.attachEvent){
emit = function(element,type,func){
element.attachEvent("on"+element,type,func)
}
}
else{
emit = function(element,type,func){
element["on"+ type] = fun;
}
}
//重写完后执行一遍
emit(element,type,func);
}
3.柯理化函数
珠峰理解: 一个大函数,返回一个小函数
柯理化函数的思想:利用闭包的机制,把一些内容事先存储和处理了,等到后期需要用的时候直接用即可
call/apply 都会把函数立刻执行, 而bind不会立刻执行函数,属于预先存储一些内容
//柯理化函数思想,大函数里面返回小函数,利用闭包机制
/*
* bind :预先处理内容
* @param func 要执行的函数,
* @param content 要更改的this指向
* @param args 给函数传递的参数
* @return 返回一个代理函数
*/
function bind(func, content, ...args) {
return function proxy() {
//call会立刻执行 bind 不兼容ie678
func.call(content, ...args);
}
}
//********************************************
let obj = {
x : 100
}
function fn(y) {
this.x += y;
console.log(this);
}
//一秒后把执行返回的小函数
//执行顺序,运行函数bind立刻执行,返回proxy给定时器,5秒后执行小函数
setTimeout(bind(fn, obj, 200), 5000);
4.函数调用扁平化 compose
let fun1 = function(x) {
return x + 10;
}
let fun2 = function(x) {
return x * 10;
}
let fun3 = function(x) {
return x / 10;
},,
//需求 拿到fun1的返回值传入fun2, 以上的返回值传入fun1 以上的返回值传入fun3 操作的函数量未知
const result = fun3(fun1(fun2(fun1(5))))//16
/**
* 函数调用扁平化
* @param {funs} 函数集合
* @param {args} 传入参数
* @return 返回汇总
*/
function compose(...funs) {
//有柯理化函数思想
//直接返回代理函数
return function proxy(...args) {
let len = funs.length;
//如果函数个数为0直接返回传参
if (len === 0) return args;
if (len === 1) return funs[0](...args);
//利用数组reduce函数实现扁平化
return funs.reduce((x, y) => {
return typeof x === "function" ? y(x(...args)) : y(x);
})
//补充知识点 如果reduce 只传入第一个函数参数,x是第一个,y是第二索引
//如果reduce传入第二个参数,x就是第二个参数,y就是第一个索引
//reduce必须有返回值,下一次循环才有x的值否则为undefined
}
}
const x = compose(fun1, fun2, fun1, fun3)(5);
console.log(x);//16
17. let const var 区别
变量提升
在当前上下文代码自上而下执行之前, 会把所有带var/function关键字的进行提前声明/定义(var 只是声明, function 是声明+定义)
更改指针
- var / let 允许变量更改指针
- const创建的变量是不能更改变量的指针
经典练习题:
要点: 循环一共创建N+1个执行上下文
// var
for (var i = 0 ; i<5; i++) {
// ...
}
console.log(i) // 5
// let
for (let i = 0 ; i<5; i++) {
// ...
}
console.log(i) // Uncaught ReferenceError: i is not defined
// 循环一共创建N+1个执行上下文
// 父上下文控制循环
// 子上下文:私有为循环体
暂时性死区
// 输出一个没有定义的变量会报错
conole.log(a) // Uncaught ReferenceError: a is not defined
// 但是检测一个没有定义的变量类型不会报错
type a !== 'undefined' // false
18. 闭包作用域练习题
function fun(n, o){
console.log(o)
return {
fun: function(m){
return fun(m, n)
}
}
}
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
19. jQuery 源码思想
1. 环境判断
通过暂时性死区特点判断对应属性是否存在, 从而判断运行环境属于浏览器还是node环境
(function (global, factory) {
"use strict";
if (typeof module === "object" && typeof module.exports === "object") {
// 此条件成立说明当前运行代码的环境支持CommonJS规范
// (浏览器端不支持/NODE端是是支持的)
module.exports = global.document ?
factory(global, true) :
function (w) {
if (!w.document) {
throw new Error("jQuery requires a window with a document");
}
return factory(w);
};
} else {
// 浏览器端运行
factory(global);
}
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
// ...
});
2. 函数暴露与缓存
假如需要把函数暴露给全局使用(闭包) 怕与别的插件/类重名,可以暴露一个方法给外部调用,变量命名谦让
function factory(){
"use strict";
var version = "3.5.1",
jQuery = function (selector, context) {
// ...
};
// 在导入JQ(但并没有把自己的jQuery/$暴露给全局),首先会把现有全局中叫做$/jQuery的存储起来,防止自己后期设置的$/jQuery会替换全局现有的
var _jQuery = window.jQuery,
_$ = window.$;
// 基于noConflict转移JQ对$/jQuery的使用权
jQuery.noConflict = function (deep) {
if (window.$ === jQuery) {
window.$ = _$;
}
if (deep && window.jQuery === jQuery) {
window.jQuery = _jQuery;
}
return jQuery;
};
// 在闭包中把一些私有的信息暴露到全局使用:RETURN/WINDOW.XXX=XXX
if (typeof noGlobal === "undefined") {
window.jQuery = window.$ = jQuery;
}
return jQuery;
}
20. This指向
THIS:执行主体(谁把他执行的)和函数在哪执行和创建没有必然的关系
函数执行,看函数前看是否有“点”,有“点”它前面是谁THIS就是谁,没有“点”THIS就是window(非严格模式)/undefined(严格模式)
+ 自执行函数中的THIS一般都是window/undefined
+ 回调函数中的THIS一般也是window/undefined(除非某个函数内部给回调函数做了特殊的处理,这样回调函数中的THIS有自己的特殊情况)给当前元素的某个事件行为绑定方法,当事件行为触发,方法中的THIS是当前操作的元素(特殊:IE6~8中基于DOM2事件绑定attachEvent,方法中的this不是元素)
箭头函数中/私有块级上下文,没有自己的THIS,所用到的THIS都是其上级上下文中的THIS(也就是没有初始化THIS这一步)
构造函数中的THIS一般是当前类的实例
基于call/apply/bind可以强制改变THIS
练习题: 闭包tihs指向图解.png
var num = 10
var obj = {
num: 20
}
obj.fn = (function (num) {
this.num = num * 3
num++
return function (n) {
this.num += n
num++
console.log(num)
}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
21. 面向对象编程(OOP)
三个概念
- 对象: 一种泛指 万物皆对象
- 类:对象的细分 ,可以细分很多个类
- 实例:类中具体的事物
备注:
所有类的基类都是Object
每一个实例都可以调用所属类(原型链)中的属性和方法
具备Symbol(Symbol.iterator)属性的, 说明它是可被迭代的数据结构, 最直观是可以使用es6的for of循环
JS中的内置类
【数据类型】
- Number String Boolean (Symbol BigInt)
- Object :Object Array RegExp Date …
- Function
- 每一个数据类型都是自己所属类的一个实例
【每一个元素对象都有一个自己所属的类】
- 每一个元素都是Object的实例
- div < HTMLDivElement < HTMLElement < Element < Node< EventTarget < Object
【元素/节点/样式集合】
- HTMLCollection / NodeList / CSSStyleDeclaration…
22. 构造函数执行的细节与知识点
new XXX做了哪些事情?
- 创建一个空的实例对象 : 当前类的实例空对象
- 初始化this , 把this指向创建的空对象
- 执行普通函数该做的事情: (作用域链/arguments/形参赋值/变量提升/代码执行)
返回创建的对象,
- 只有return 引用类型才会返回对应引用类型的,
- 否则 全部返回创建的实例对象: return基本类型 或者 不return
验证
- in 判断 : 不论是私有还是公有属性, 只要在原型链上找到, 就返回true
- hasOwnProperty 判断: 只有在它的私有属性, 结果才为true
- instanceof 判断 : 判断某某是不是某某的实例 ```javascript function Func(x, y) { let total = x + y; this.result = total; this.say = function () {}; }
let f1 = new Func(10, 20); console.log(f1); console.log(f1.total); //=>undefined total只是上下文中的私有变量,和实例没有必然的关系,只有this.xxx=xxx才是给实例设置的私有属性 console.log(f1.result); //=>30
console.log(‘say’ in f1); //=>true console.log(‘toString’ in f1); //=>true console.log(f1.hasOwnProperty(‘say’)); //=>true console.log(f1.hasOwnProperty(‘toString’)); //=>false
<a name="g1jvV"></a>
#### 对象相关
```javascript
Object.assign()//合并对象
Object.create(Obj) //创建一个空对象把__proto__指向Obj
Object.defineProperty(obj,key,options)//监听一个对象某个属性
Object.entries()//获得一个对象的所有键值对
Object.keys()//获取键
Object.values()//获取值
//const创建的变量是不能更改变量的指针
Object.freeze()//冻结一个对象不能所有操作增删改 只限于指定层
Object.is()//传入两个实参,判断两个值是否相等,比===更准确
23. 原型/原型链
核心理解:
- 每一个函数(构造函数[类])都天生具备一个属性“prototype原型”,属性值是一个对象:存储当前类供实例调用的公共属性和方法
- 在原型对象上有一个内置的属性“constructor构造函数”,存储的值是当前函数本身,所以我们把类称为构造函数
- 每一个对象都天生具备一个属性“proto隐式原型/原型链”,属性指向自己所属类的原型对象
- 实例.proto===所属类.prototype
函数类型:
- 普通函数
- 构造函数(类)
- 内置类
对象类型:
- 普通对象/数组对象/正则对象/日期对象…
- prototype/proto
- 类的实例也是对象(排除基本数据类型值的特殊性)
- 函数也是对象
- 万物皆对象
原型链机制:
访问对象的某个成员, 首先查看是否为私有的, 如果是私有的, 则找私有的, 如果没有, 则基于porto找所属类的.prototype上的公共成员(属性/方法), 如果还是没有, 再基于proto 继续往上查找, 直到Object.prototype为止, 如果都没有则返回属性(undefined)/方法(报错)
图解: 内置类的原型图.png
练习: 题目2图解.png
// 1
console.log(
Array.prototype.hasOwnProperty('push')
)
// 2
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.getY = function () {
console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
链式写法:为什么可以实现链式写法
上一个方法执行返回的结果依然是当前类的实例;
25. new方法重写
- 创建指向传入函数原型的空对象: Object.create()
- call执行
- 判断call执行返回的数据类型
关联22标题
技巧:
- 创建一个纯空对象 const obj = Object.create(null)
```javascript
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log(“wangwang~~”);
}
Dog.prototype.sayHi = function() {
console.log(this.name);
}
/**
- 自己实现的new
- @param {func} 操作的类
- @param {args} 参数 */ function new(func, …args) { // //默认创建一个实例对象,(属于当前这个类的实例) // let obj = {} // //把创建的原型等于操作类的原型(继承) // obj.proto = func.prototype; //以上IE大部分都不允许我们直接操作_proto //解决方案 let obj= Object.create(func.prototype); //也会把类当做普通函数执行 //执行的时候要保证函数中的this指向创建的实例obj let result = func.call(obj, …args); //判断用户是否有自己的引用类型返回值,是引用类型返回用户,否者返回新对象 if (result !== null && /^(object|function)$/.test(typeof result)) { return result; } return obj; }
let sanmao = _new(Dog,”sanmao”); sanmao.bark();//wangwang~~ sanmao.sayHi();//sanmao
<a name="vssDb"></a>
#### Object.create重写
```javascript
/*
* Object.create([OBJECT])
* 创建一个空对象x,并且把[OBJECT](这个值需要是一个对象)作为新对象的原型指向
* x.__proto__=[OBJECT]
*
* Object.create(null):创建一个没有原型/原型链的空对象(不是任何类的实例)
*/
// 我们写的这个方法不支持null的处理
Object.create = function create(prototype) {
if (prototype === null || typeof prototype !== "object") {
throw new TypeError(`Object prototype may only be an Object: ${prototype}`);
}
// 创建一个类,创建这个类的实例,实例.__proto__=类.prototype;而我们让类.prototype等于传递的prototype;
function Temp() {}
Temp.prototype = prototype;
return new Temp;
};
26. 内置类原型扩展注意点
- 为了防止自己设定的方法覆盖内置的方法, 我们设置的方法需加前缀
- 优势: 使用起来方便, 和内置方法类似, 直接让实例调用即可
- 优势: 方法中的this一般是当前要操作的实例, 也就不需要传递操作对象
- 只要保证方法的结果还是当前类的实例, 我们就可以使用链式操作 调用内置的方法
29. Function与Object之间的关系
- Object座位一个类/函数, 它是Function的一个实例
- Function虽然是类/函数, 但是函数也是对象.所以也是Object的一个实例
- Js中任何实例(除开基本类型) 最后都可以基于自己的proto 找到Object.prototype 也就是说所有的值都是Object的实例 —> 万物皆对象 ```javascript // 大写函数是大写函数的实例 console.log( Function instanceof Function // true )
// 自己的原型链等于自己的原型 console.log( Function.prototype === Function.porto // true )
console.log( Object.proto.proto === Object.prototype // true )
图解: [Function与Object的关系.png](https://www.yuque.com/attachments/yuque/0/2021/png/1665156/1636953334585-ef5ea4f5-0711-4de7-a4be-228c4a33f5ac.png?_lake_card=%7B%22src%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2021%2Fpng%2F1665156%2F1636953334585-ef5ea4f5-0711-4de7-a4be-228c4a33f5ac.png%22%2C%22name%22%3A%22Function%E4%B8%8EObject%E7%9A%84%E5%85%B3%E7%B3%BB.png%22%2C%22size%22%3A306686%2C%22type%22%3A%22image%2Fpng%22%2C%22ext%22%3A%22png%22%2C%22status%22%3A%22done%22%2C%22taskId%22%3A%22u07303ee1-68d1-4c66-bdcd-a63c6e5c106%22%2C%22taskType%22%3A%22upload%22%2C%22id%22%3A%22u01003aab%22%2C%22card%22%3A%22file%22%7D)
<a name="zwhxI"></a>
## 30. 原型重定向细节
| 方法 | 缺点 | 优点 |
| --- | --- | --- |
| <br />1. 向内置的原型上扩展方法<br /> | 编写繁琐,代码不通用 | 不会影响原来的原型 |
| <br />2. 直接对象赋值原型<br /> | 旧原型对象的方法全部没了 | 代码便捷,美观 |
| <br />3. 使用Object.assign<br /> | 如果成员重名, 会丢失一个 | 不重名前提下,没啥毛病 |
| <br />4. Object.create跟Object.assign结合<br /> | 无 | 技能合并方法, 重名也不会丢失 |
```javascript
function Func() {}
Func.prototype.XX = function () {};
方法一
// 2.设置别名
Func.prototype.A = function () {};
Func.prototype.B = function () {};
let proto = Func.prototype;
proto.A = function () {};
proto.B = function () {};
// 方法二
// 一般想往原型上批量设置属性方法,都是基于重定向的方式
// 1.缺失了constructor
// 2.也缺失了原始原型对象的上的属性和方法
Func.prototype = {
A: function () {},
B: function () {}
};
// 方法三
// 两个原型对象合并,用新的原型对象替换原始的原型对象(问题:如果新老有个属性方法相同,则新的值会替换老的值)
Func.prototype = Object.assign(Func.prototype, {
A: function () {},
B: function () {}
});
// 方法四
// 3.把老的原型对象作为新原型对象的上级原型
let protoNew = Object.create(Func.prototype);
protoNew = Object.assign(protoNew, {
A: function () {},
B: function () {}
});
Func.prototype = protoNew;
练习题:
function Fn() {
let a = 1;
this.a = a;
}
Fn.prototype.say = function () {
this.a = 2;
}
Fn.prototype = new Fn;
let f1 = new Fn;
Fn.prototype.b = function () {
this.a = 3;
};
console.log(f1.a);
console.log(f1.prototype);
console.log(f1.b);
console.log(f1.hasOwnProperty('b'));
console.log('b' in f1);
console.log(f1.constructor == Fn);
图解: 原型重定向.png
32.函数的三种角色
函数与对象之间的关系
- 函数与类都有prototype
函数的角色
1 . 普通函数 : 作用域/作用域链
2 . 构造函数 : 类 / 实例 / 原型/ 原型链
3 . 普通对象 : 键值对操作
主要角色: 函数本身
typeof Array // 'function'
typeof Object // 'function'
练习题:
考点: 变量提升, 作用域链, this指向, 函数的角色, 运算符优先级
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
图解: 练习题图解.png
33. 多种继承方式
原型继承
核心: Child.prototype = new Parent
特点:
- 父类中私有和公有的成员, 最后都变为子类实例公有的成员
- 和其他语言不同的是, 原型继承并不会把父类的成员”拷贝”给子类, 而是通过原型链方式建立连接关系
- 原型继承图解.png
call继承
核心: Parent.call(this) 在子类构造函数中call执行父类方法,(普通方法执行)
特点:
- 能把父类的私有属性’拷贝’为子类的私有属性
- 父类的公共成员与子类没有关系(缺点)
- call继承图解.png
寄生组合式继承
核心: call继承+ 另类原型继承
function Parent() {
this.x = 100
}
Parent.prototype.getx = function getx() {
return this.x
}
function Child() {
Parent.call(this) // 核心1
this.y = 200
}
Child.prototype = Object.create(Parent.prototype) // 重点
Child.prototype.constructor = Child // 锦上添花
Child.prototype.getY = function getY() {
return this.y
}
let c1 = new Child
console.log(c1)
特点:
- 父类的私有成员’拷贝’为子类的私有成员
- 父类的共有成员’链式指向’为子类的共有成员
- 寄生组合式继承.png
Es6方式extends继承
核心: extends super()
特点:
与Es5寄生式组合继承很相似 ```javascript class Parent { constructor() { this.x = 100 }
getX() { return this.x } }
class Child extends Parent { constructor() { // 一定要超链方法调用 super() this.y = 200 }
getY() { return this.y }
}
const c1 = new Child
console.log(‘c1 :’, c1)
<a name="Q1M7J"></a>
## 34. 面向对象练习题
<a name="HUUkw"></a>
#### 1. toArray
接收未知个参数, 最后输出数组 toArray
```javascript
// 单例
let utils = (function () {
/*
* toArray:转换为数组的方法
* @params 不固定数量,不固定类型
* @return [Array] 返回的处理后的新数组
* by zhufengpeixun on 2020
*/
function toArray() {
// return Array.from(arguments);
// return [...arguments];
// 把类数组转换为数组
// var arr = [];
// for (var i = 0; i < arguments.length; i++) {
// arr.push(arguments[i]);
// }
// return arr;
// 只要两个实例结构类似,那么大部分操作操作的他们的代码都可以公用,无外乎就是THIS指向的问题
return [].slice.call(arguments);
}
return {
toArray
};
})();
let ary = utils.toArray(10, 20, 30); //=>[10,20,30]
console.log(ary);
ary = utils.toArray('A', 10, 20, 30); //=>['A',10,20,30]
console.log(ary);
2. 深克隆
接收一个参数, 进行深克隆
解决方式一:
普通深克隆暴力法 JSON.parse(JSON.stringify(obj))
缺点:
属性类型 | 使用方式一现象 |
---|---|
1. 正则 |
变为空对象 |
2. 函数 |
直接消失 |
3. 日期 |
直接转为字符串 |
4. Symbol |
直接消失 |
5. BigInt |
直接报错 |
6. undefined |
直接消失 |
解决方式二
基本满足,
未处理: 循环引用会死循环, 还有很多类型没处理(Promise、Buffer、Set、Map …
/**
* 思路:
* 1. 基本类型直接返回
* 2. 处理日期/正则
* 3. 普通对象for in 循环, 递归克隆属性
* @param obj
* @returns {*}
*/
const cloneDeep = function (obj) {
const constructor = obj.constructor
// 基本类型
if (obj == null || typeof obj !== 'object') {
return obj
}
// 日期, 正则
if (/^(RegExp|Date)$/i.test(constructor.name)) {
return new constructor(obj)
}
// 对象
let clone = new constructor
for (const key in obj) {
// 循环私有成员
if (!obj.hasOwnProperty(key)) break
clone[key] = cloneDeep(obj[key])
}
return clone
}
3. toType
js中的数据类型检测
- typeof 检测普通基本类型值有效
- 缺点: null、数组、对象、正则、日期 ==> ‘object’
- instanceof 检测xxx是不是xxx的实例
- 缺点: 只要出现在实例的原型链上的类检测, 结果就为true
- 例子: arr1 instanceof Object ==> true
- constructor 通过判断实例的构造函数是否等于xxx类
- 缺点: 构造函数是可以更改的
- 例子: arr.construtor = {} ; arr.constructor === Array ==> false
- Object.prototype.toString.call(xxx)
- 缺点: 写太长了 - -!
/**
* 思路:
* 1. 先使用typeof判断 如果是不是object类型 直接返回
* 2. 否则使用Object.prototype.toString.call 判断类型
* 3. 截取object 后的类型
* @param obj 任意类型
* @returns {"undefined"|"boolean"|"number"|"string"|"function"|"symbol"|"bigint"|string}
*/
const toType = function(obj){
let _typeOf = typeof obj
if (_typeOf !== 'object') return _typeOf
const _toStringType = Object.prototype.toString.call(obj)
const [_, type] = /^\[object ([a-z]+)\]$/i.exec(_toStringType)
return type.toLowerCase()
}
4. 深判断 isEqual
/**
* 功能: 传递两个参数 判断两个参数是否完全一样,深判断
* 思路:
* 1. 基本类型值使用 === 判断
* 2. 函数, 日期, 正则 toString()后比较
* 3. 对象, 挨个比较每个属性跟属性值
* @param val1
* @param val2
* @returns {boolean}
* @private
*/
const _isEqual = function (val1, val2) {
const type1 = val1 === null ? 'null' : typeof val1
const type2 = val2 === null ? 'null' : typeof val2
const toStringJudge = val1.toString() === val2.toString()
// 函数 => toString()
if (type1 === 'function' && type2 === 'function') {
return toStringJudge
}
// 对象
if (type1 === 'object' && type2 === 'object') {
const ct1 = val1.constructor
const ct2 = val2.constructor
const isRegExp = ct1 === RegExp && ct2 === RegExp
const isDate = ct1 === Date && ct2 === Date
// 日期, 正则
if (isRegExp || isDate) {
return toStringJudge
}
// 其他对象
const keys1 = Object.keys(val1)
const keys2 = Object.keys(val2)
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1; i++) {
const key1 = keys1[i]
const key2 = keys2[i]
if (key1 !== key2) return false
const item1 = val1[key1]
const item2 = val1[key2]
// 递归
const flag = _isEqual(item1, item2)
if (!flag) return false
}
return true
}
// 基本类型
return val1 === val2
}
37. instanceof 重写
规则: 只要在实例的原型链上找到类的原型, 就会返回true
/**
* 思路:
* 1. 使用while循环, 每次往上查找原型链
* 2. 如果找到原型链 === null 已经到尽头退出
* 3. 如果原型链 === 类的原型 返回true
* @param example 实例
* @param classFun 类
* @returns {boolean}
* @private
*/
const _instanceOf = function (example, classFun) {
const classPrototype = classFun.prototype
let proto = Object.getPrototypeOf(example)
while (true) {
if (proto === null) return false
if (proto === classPrototype) return true
proto = Object.getPrototypeOf(proto)
}
}
38. 原型上扩展方法事项
优点:
- 可以直接让实例调用,而此时方法执行,方法中的THIS是调用的实例(调取比较的方便)
- 可以实现链式写法:上一个方法执行返回的结果,可以继续调取本结果所属类原型上的方法
注意:
- 我们自己扩展的方法不要覆盖内置的方法(最好设置的方法带个前缀)
要点:
- 创建值的方式分为两种,
- 字面量方式 const num = 10
- 构造函数方式 const num = new Number(10)
- 创建基本类型的值时
- 字面量方式创造的基本类型的值、
- 构造函数创造出来的是引用数据类型的值
- 在不改变this的前提下, this一般是一个对象(除了null/undefined)
- 基本类型在运算时会先调取.valueOf获取原始值[[PrimitiveValue]]
练习题:
- 原型添加方法, 链式操作 let m = n.plus(10).minus(5); console.log(m); //=>15(10+10-5) ```javascript // 处理参数 function initParams(num) { num = Number(num); return isNaN(num) ? 0 : num; } Number.prototype.plus = function plus(num) { // this -> 永远是对象数据类型的值(除了null/undefin ed) // 对象+数字:不一定都是字符串拼接 // =>对象本身是想转换为数字再进行运算的 // =>对象转换为数字,并不是先toString,他需要先.valueOf获取原始值[[PrimitiveValue]],如果有原始值(原始值是基本类型的值),直接基于原始值处理,没有原始值的才去toString\ num = initParams(num); return this + num; }; Number.prototype.minus = function minus(num) { num = initParams(num); return this - num; };
let n = 10; let m = n.plus(10).minus(5);
2. queryUrlParams 查询 根据传入key 查询url的参数值
```javascript
// 利用正则捕获
String.prototype.queryUrlParams = function (key) {
const obj = {}
this.replace(/([^?=&#]+)=([^?=&#]+)/g, (_, key, value) => obj[key] = value)
this.replace(/#([^?=&#]+)/g, (_, hash) => obj['_HASH'] = hash)
return key ? obj[key] : obj
}
使用Es6+编写一个类
class Model { constructor(x, y) { // 给实例添加私有属性 this.x = x this.y = y } // Es7+ 给实例设置私有属性 z = 100 // 给其原型添加方法 (不能设置属性) getX() { console.log('this.x :', this.x) } getY() { console.log('this.y :', this.y) } // Es7+ // 把Model 当为普通对象身份添加键值对(静态属性方法) // 跟实例没有关系 static n = 200 static setNumber(n) { this.n = n } }
39. 面向对象的变相应用
练习题:
var a = ? 的时候 if ( a == 1 && a==2 && a ==3 )
- 方案一: 对象转换为数字, 需要调取valueof/ toString(如果对象中有私有toString, 则不会往上查找)
- 方案二: 通过数据劫持 ```javascript // 方案一, 把转换方法私有化 var a = { i: 0, toString() { return ++this.i } } // 或者 var a = [1,2,3] a.toString = a.shift
// 方案二: 数据劫持 var i = 1 Object.defineProperty(window, ‘a’, { get() { return ++i } })
// 题目 // var a = ? if(a == 1 && a == 2 && a == 3) { console.log(‘Ok’) }
<a name="IJF6Y"></a>
## 40. call 重写
思路:
1. 判断传入第一个参数是否为null/undefined, 若是this指向window
1. 判断传入第一参数是否为Object/Function,若不是第一参数转为对象
1. 创建Symbol,第一个参数的Symbol对象赋值为this
1. 调用第一参数的Symbol方法传入剩余参数return返回值, 删除Symbol属性
```javascript
/**
* @param _thisObj 要改变this的指向对象
* @param params 传入的剩余参数
*/
Function.prototype.myCall = function myCall(_thisObj, ...params) {
_thisObj = _thisObj == null ? window : _thisObj
const _thisObjType = typeof _thisObj
if (!/^(object|function)$/i.test(_thisObjType)) {
// _thisObj = new _thisObj.constructor(_thisObj) // 不适合Symbol和bigInt类型
_thisObj = Object(_thisObj)
}
let result, key = Symbol('KEY')
// this指向点前的调用者
_thisObj[key] = this
result = _thisObj[key](...params)
// 处理完需要删除添加的对象
delete _thisObj[key]
return result
}
// 栗子
function fun(x, y) {
const sum = x + y
console.log('sum :', sum, this)
}
// 传入 对象, 方法, 字符串, 数字, 数组, BigInt, Symbol, 布尔
const any = {}
fun.call(any , 1, 2)
fun.myCall(any, 1, 2)
练习题:
function A(x, y) {
console.log(x + y, this.name)
}
function B(x, y) {
console.log(x - y, this.name)
}
B.call(A, 10, 20) // -10 , A
B.call.call.call(A, 10, 20) // NaN undefined
B.call.call.call.call.call.call(A, {name: '杨明'}, 20, 10) // 30 杨明
Function.prototype.call(A, 10, 20) // 不输出 匿名空函数
Function.prototype.call.call.call(A, 10, 20) //NaN undefined
.call.call.call....其实都是call本身
// 一次call就是调用call前面的方法
// 多次call就是调用传递第一个参数
1.png 图解
41. bind 重写
call/apply:立即执行函数并且修改里面的THIS
bind:cccc
- 利用柯理化函数的编程思想,
- 预先把 “需要处理的函数/改变的THIS/传递的实参” 等信息存储在闭包中,
- 后期到达条件(事件触发/定时器等),先执行返回的匿名函数,cc
- 在执行匿名函数的过程中,再去改变THIS等 =>THIS和参数的预处理 ```javascript Function.prototype.myBind = function bind(context, …params) { // this -> 处理的函数 func // context -> 要改变的函数中的THIS指向 obj // params -> 最后给函数传递的实参 [10,20] let _this = this return function anonymous(…args) { // args -> 可能传递的事件对象等信息 [MouseEvent] // this -> 匿名函数中的THIS是由当初绑定的位置触发决定的(总之不是func要处理的函数) _this.call(context, …params.concat(args)) } }
const obj = { name: ‘zhufeng’ } document.body.onclick = func.myBind(obj, 10, 20)
<a name="YUv3H"></a>
## 43. 防抖和节流
<a name="WqEq3"></a>
#### 1. 防抖
概念: 在用户频繁触发的时候,我们只识别一次(识别第一次/识别最后一次)
栗子: 第一次点击,没有立即执行,等待500MS,看看这个时间内有没有触发第二次,有触发第二次说明在频繁点击,不去执行我们的事情(继续看第三次和第二次间隔...);如果没有触发第二次,则认为非频繁点击,此时去触发;
```javascript
/**
* 防抖函数
* 思路:
* 1. 利用柯理化思想 缓存timer定时器
* 2. 每次触发事件时, 清掉上一次定时器的函数
* 3. 假如定时器函数执行过, timer = null 作为下次防抖的依据
* 3. 通过 immediate 控制立刻执行还是末尾执行
* @param func 要执行的函数
* @param wait 防抖的时间
* @param immediate 是否为立刻执行
* @return {anonymous}
*/
const debounce = function (func, wait, immediate) {
let timer = null
return function anonymous(...params) {
const now = immediate && !timer
clearTimeout(timer)
timer = setTimeout(_ => {
timer = null
!immediate ? func.call(this, ...params) : null
}, wait)
now ? func.call(this, ...params) : null
}
}
2. 节流
概念: 函数频繁触发, 每一段时间内执行一次 (目的为了缩减频率)
/**
* 函数节流
* 功能: 频繁触发, 每wait时间内只执行一次,
* 目的: 降低频率
* 思路:
* 1. 柯理化思想
* 2. 记录上一次执行的时间 跟 下一次时间比较,
* 3. 通过判断剩余时间小于0时且timer不存在, 立刻执行, 否则 添加定时器等待执行
* 4. 执行完函数记录当前执行的时间来往返判断
* @param func 节流执行的函数
* @param wait 时间间隔
* @return {anonymous}
*/
function throttle(func, wait = 500) {
let timer = null
//记录上一次操作时间
let previous = 0
return function anonymous(...params) {
//当前操作的时间
let now = new Date()
const remaining = wait - (now - previous)
if (remaining <= 0) {
// 两次间隔时间超过频率:把方法执行即可
clearTimeout(timer)
timer = null
previous = now
func.call(this, ...params)
} else if (!timer) {
// 两次间隔时间没有超过频率,说明还没有达到触发标准呢,设置定时器等待即可(还差多久等多久)
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
previous = new Date()
func.call(this, ...params)
}, remaining)
}
}
}
44. 闭包, 面向对象 综合
1. 闭包
- 什么是闭包, 以及它的产生
- 堆栈内存
- ECStack、 EC、 VO、 AO、 Scope、 ScopeChain
- 浏览器垃圾回收机制, 引用计数(ie) 标记清除(chrome)
- 闭包的优缺点以及作用
- 优点: 保护, 保存
- 缺点: 占用内存, 上下文栈不释放
- 应用场景
- 循环事件绑定
- let 块级作用域
- 单例设计模式
- 高阶函数: 惰性函数, 柯理化函数, compose函数
- 源码分析
- JQuery源码
- 函数的防抖和节流
- bing
- ….
- 自我开发的插件
2. 面向对象
- 基础理论
- 面向对象和面向过程的区别
- 类与实例, (包含类的封装继承多态)
- new构造函数, new的原理
- 原型与原型链 ( proto 和 prototype )
- 应用场景
- 应用他的思想 借用数组的方法 实现类数组的处理
- 数据类型检测
- 重写原型
- 插件组件的封装
- 源码分析
- JQuery
- Lodash
- Vue
- …