base

1、实现call 和 apply

使用

1
2
3
4
5
6
7
8
9
function getName (lastName) {
return this.firstName + lastName
}

const obj = {
firstName: 'hello'
}

getName.call(obj, 'world') // helloworld

原理

  • 在Function.prototype上添加方法,是所有 function 都继承的方法

  • 任何一个函数调用Function.prototype上的方法时,this指向调用函数

1
2
3
4
5
6
7
Function.prototype.call = function(thisArg, ...args) {
thisArg = thisArg || window
thisArg._temp = this // this指向调用call的函数
const result = thisArg._temp(...args) // 作为对象的方法调用
delete thisArg._temp // 删除临时
return result
}

理解

1
2
3
4
5
6
getName.call(obj, 'world')

// 等价于
obj._temp = getName
const result = obj._temp('world')
delete obj._temp

apply 只是参数变一下即可

2、实现bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数

但我们忽略了三点:

bind()除了this还接收其他参数,bind()返回的函数也接收参数,这两部分的参数都要传给返回的函数

如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例

没有保留原函数在原型链上的属性和方法

使用

1
2
3
4
5
6
function aa () {
return this.name
}

var newFn = aa.bind({name: 333})
console.log(newFn())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Function.prototype.myBind = function(thisArg) {
if (typeof this !== 'function') {
return;
}
var _self = this;
var args = Array.prototype.slice.call(arguments, 1)
var fnBound = function () {
// 如果当前函数的this指向的是构造函数中的this 则判定为new 操作
var _this = this instanceof _self ? this : thisArg;
return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)));
}
// 为了完成 new操作
// 还需要做一件事情 执行原型 链接 (思考题,为什么?
fnBound.prototype = this.prototype;
return fnBound;
}

//测试
const obj = { name: '写代码' }
function foo() {
console.log(this.name)
console.log(arguments)
}

foo.myBind(obj, 'a', 'b', 'c')()

实现new

new 做了什么

  • 创建一个新对象,并继承其构造函数的prototype,这一步是为了继承构造函数原型上的属性和方法

  • 执行构造函数,方法内的this被指定为该新实例,这一步是为了执行构造函数内的赋值操作

  • 返回新实例(规范规定,如果构造方法返回了一个对象,那么返回该对象,否则返回第一步创建的新对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CNew(fn, ...args) {
// 等价 obj.__proto__ = fn.prototype
let obj = Object.create(fn.prototype)

// 执行构造方法, 绑定this指向
let result = fn.apply(obj, args)

// 如果构造方法已经return了一个对象,那么就返回该对象
return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}

function Foo(name) {
this.name = name
}

const s = CNew(Foo, 'liu')
console.log(s)

Object.create

Object.create(proto, propertiesObject)方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

  • proto 新对象原型

  • 返回:一个新对象,带着指定的原型对象和属性

1
2
3
4
5
6
7
8
9
10
// 实现 Object.create
function ac (arg) {
function Fn() {}
Fn.prototype = arg
const obj = new Fn()
if (arg === null) {
obj.__proto__ = null
}
return obj
}

Object.create

深度拷贝

避免循环引用,利用 map 可以使用对象作为键的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepCopy(obj, map = new Map()) {
if (typeof obj === 'object') {
let res = Array.isArray(obj) ? [] : {};
if(map.get(obj)){
return map.get(obj);
}
map.set(obj,res);
for(var i in obj){
res[i] = deepCopy(obj[i],map);
}
return map.get(obj);
}else{
return obj;
}
};

事件机制

捕获 -> 目标阶段 -> 冒泡

捕获阶段:document -> html -> body -> div

目标阶段:div

冒泡阶段:div -> body -> html -> document

事件监听

addEventListener的第三个参数默认false,表示事件冒泡阶段调用事件处理函数;如果为true,则表示在事件捕获阶段调用事件处理函数

阻止冒泡

  • event.cancelBubble

  • event.stopPropagation()

事件对象的 target 和 currentTarget

target:是当前点击的 dom 元素

currentTarget: 是绑定 click 事件的元素

面向对象继承

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent() {
this.name = 'parent'
}

Parent.prototype.getName = function() {
return this.name
}
function Child() {}

Child.prototype = new Parent()

// 绑定一下 constructor
Child.prototype.constructor = Child

const child = new Child()
child.name
child.getName()
  • 如果你打印如下代码,你会发现函数(构造函数)是有个圆形属性 prototype,prototype 下有个 constructor 指向函数本身

  • 当 Child.prototype 被粗暴的赋值后,会导致 constructor 指向丢失,所以才会有上面的绑定一下 constructor 操作

1
console.dir(function Foo () {})

缺点:

  • 所有Child原型都指向同一个Parent实例,对某个Child实例的父类引用类型变量修改会影响所有的Child实例

  • 在创建子类实例时无法向父类构造传参

构造继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function() {
return this.name
}

function Child(arg) {
Parent.call(this, arg)
}

const child1 = new Child('liu')
child1.getName // undefined

缺点:

  • 继承不到父类原型上的属性和方法

组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function() {
return this.name
}
function Child(arg) {
Parent.call(this, arg)
}

Child.prototype = new Parent()
Child.prototype.constructor = Child

const child1 = new Child('liu')

console.log(child1.name)
console.log(child1.getName())

缺点:

  • 执行了两次构造函数(Parent.call()和new Parent())生成了副本

寄生式组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent(name) {
this.name = [name]
}

Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan')
}

// Object.create 进行浅拷贝,并指向父类原型
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName()

原型链

  • 原型链起点来自两处:Object.prototype (对象) 和 Object.proto (匿名函数),找到头绪才能捋清线

  • Object.prototype.__proto__ 指向 null 也就是最顶层; Object.__proto__.__proto__ 指向 Object.prototype 对象

  • function Aa () {} 一个普通的构造函数,本身具有 prototype 属性,对应的 Aa.prototype.__proto__ 指向起点 Object.prototypeAa.__proto__指向 Object.__proto__

  • 经过实力化对象var a = new Aa(),a 对象不在具有 prototype 字段,而是 a.proto,a.proto就是构造函数Aa.prototype

  • 如果直接声明对象 var x = {},则a.proto 指向 Object.prototype

class 实现继承

先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A {
constructor () {
this.unique = 'unique'
}
name = 'a'
static age = '5'
static getAge = () => {
return this.age
}
something () {
return 3
}
getName = () => {
return this.name
}
}

class B extends A {
Bname = 'b'
}
var newB = new B()
console.log(newB)
console.log(newB.age) // undefined
console.log(newB.getName()) // a

// 输出
B {
name: "a",
unique: "unique",
Bname: "b",
getName: ƒ,
__proto__: A
}
  • 静态属性和方法不会被被继承

  • 属性和表达式的方法 getName (被理解成属性赋值了一个函数)会被生成实例化的副本

  • 声明式的方法会保持在原型上

阮老师的es6上也提到:构造函数的 prototype 属性,在 ES6 “类”上面继续存在。类的所有方法都定义在类的prototype属性上面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
constructor() {
}
toString() {}
// 这种写法不会在原型上,该写法被理解为属性
getName = () => {}
}

// 等同于
Point.prototype = {
constructor() {},
toString() {}
};

//

typeof 和 instanceof

typeof 用来判断基本类型,null比较特殊是object

instanceof:

1
2
3
4
5
6
7
8
9
10
11
12
13
function new_instance_of(leftVaule, rightVaule) { 
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}

js类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Object.prototype.toString.call({})             // [object Object]

Object.prototype.toString.call([]) // [object Array]

Object.prototype.toString.call(function(){}) // [object Function]

Object.prototype.toString.call('') // [object String]

Object.prototype.toString.call(1) // [object Number]

Object.prototype.toString.call(true) // [object Boolean]

Object.prototype.toString.call(null) // [object Null]

Object.prototype.toString.call(undefined) // [object Undefined]

Object.prototype.toString.call() // [object Undefined]

Object.prototype.toString.call(new Date()) // [object Date]

Object.prototype.toString.call(/at/) // [object RegExp]

for in 和 for of

  • for in会遍历所有的可枚举属性,包括原型

  • for of遍历的是数组元素值

1
2
3
4
5
6
7
8
9
Array.prototype.sayHello = function(){}
var myArray = ['a', 'b', 'c']
for(let key in myArray){
console.log(key)
}
// 0
// 1
// 2
// sayHello

v8

js 引擎:

  • Chakra(前 Edge JavaScript 引擎,新版转向 Chromium 内核,V8引擎)

  • JavaScript Core(Safari)

  • SpiderMonkey(Firefox)

V8主要内容:

  • Ignition(基线编译器)快速生成字节码进行执行

  • TurboFan(优化编译器)

  • Orinoco(垃圾回收器)

执行顺序:

1
js code -> 词法分析 -> 生成token字节流 -> 语法分析 -> 是否有错误(有则抛出) -> 生成 AST (抽象语法树) -> 根据AST生成字节码 -> 执行字节码 -> 发现有热点函数,V8会启用Turbofan优化编译,直接生成机器码
  • 解析代码需要时间,JavaScript 引擎会尽可能避免完全解析

  • 另外,通过用户交互行触发的动作不会立即被执行,所有主流浏览器都实现了惰性解析(Lazy Parsing)

  • 解析器可以决定“预解析”(Pre-parsing)或“完全解析”它所遇到的函数;预解析会检查源代码的语法并抛出语法错误,但不会解析函数中变量的作用域或生成 AST;完全解析则将分析函数体并生成源代码对应的 AST 数据结构

语法树在线解析工具

垃圾回收

任何垃圾回收器都有一些必须定期完成的基本任务

  • 确定存活/死亡对象

  • 回收/再利用死亡对象所占用的内存

  • 压缩/整理内存(可选)

V8 垃圾回收基于世代假说,将内存分为新生代和老生,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长

名称 算法 大小
新生代 Parallel Scavenge 算法 32MB(64位)/ 16MB(32位)
老生代 标记清除、标记整理算法 1400MB(64位)/ 700MB(32 位)

新生代内存回收机制:

  • V8 将新生代拆分为大小相同的两个半空间,分别称为 Form 空间 和 To 空间;

  • 垃圾回收时,V8 会检查 From 空间中的存活对象,将这些对象复制到 To 空间。

  • 之后,V8 将直接释放死亡对象所对应的空间。每次完成复制后,From 和 To 的位置将发生互换

新生代分 Nursery 和 Intermediate 子世代。新对象被分配到 Nursery ,对象在第一次垃圾回收中存活,进入 Intermediate ,下一次垃圾回收中再次存活,就会进入老生代

老生代内存回收机制:

  • 标记清除:垃圾回收器从根节点开始,标记根直接引用的对象,然后递归标记这些对象的直接引用对象,判断是否存活

  • 标记整理:标记清除之后,就可能会产生内存碎片,整理碎片内存

数据类型与存储

  • 原始值大小固定,因此保存在栈内存上

  • 引用值是对象,存储在堆内存上

返回
顶部