new、instanceof
- 创建空对象
- 继承构造函数原型:对象的原型指向构造函数的原型对象
- 执行构造函数,绑定该对象为this
- 返回对象
function customNew(constructor: Function, ...args: any[]) {
const obj = {};
obj.__proto__ = constructor.prototype
const res = constructor.apply(obj, args);
return ((typeof res === "object" || typeof res === "function") && res !== null) ? res : obj
}
注意兼容处理
function customInstanceof(instance: any, origin: any) {
if (instance === null || instance === undefined) {
return false
}
if (typeof instance !== 'object' && typeof instance !== "function") {
return false
}
while (instance) {
if (instance.__proto__ === origin.prototype) {
return true
} else {
instance = instance.__proto__
}
}
return false
}
bind、call、apply
- 绑定this和部分参数(可以一次绑定,或者二次), 箭头函数不能绑定this
- 返回新函数,不执行
Function.prototype.customBind = function () {
const args = [...arguments];
const ctx = args[0];
let params = args.slice(1);
const that = this;
return function () {
params = [...params, ...arguments]
return that.apply(ctx, params)
}
}
// 绑定this、传入执行参数
Function.prototype.customCall = function (ctx, ...args) {
// 类型兼容, 空指向window | global, 函数还是本身
if (ctx === null || ctx === undefined) {
ctx = globalThis
}
// 值类型,会被转换。比如 1 => Number{ 1 }
if (typeof ctx !== "object") {
ctx = new Object(ctx)
}
// 使用唯一key, 防止重名污染参数
const symbolKey = Symbol();
ctx[symbolKey] = this;
const res = ctx[symbolKey](...args);
delete ctx[symbolKey];
return res
}
// 实现同call, 参数不同
Function.prototype.customApply = function (ctx, args) {
if (ctx === null || ctx === undefined) {
ctx = globalThis
}
if (typeof ctx !== "object") {
ctx = new Object(ctx)
}
const symbolKey = Symbol();
ctx[symbolKey] = this;
const res = ctx[symbolKey](...args);
delete ctx[symbolKey];
return res
}
防抖、节流
防抖:什么时候停止,什么时候执行函数,场景:输入框的模糊搜索、按钮点击等
function debounce(fn, delay = 100) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
节流:每个时间段,只执行一次,场景:事件触发频繁(如滚动),触发过程又要执行函数
// 比对时间戳
function throttle(fn, time = 100) {
let oldTime = null
return function (...args) {
const newTime = new Date().getTime()
if (newTime - time > oldTime) {
oldTime = newTime
fn.apply(this, args)
}
}
}
// 记录延时器
function throttle(fn, time = 100) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, time)
}
}
}
getType
// 获取数据类型
function getType(val: any): string {
return Object.prototype.toString.call(val).split(" ")[1].replace("]", "").toLowerCase()
}
数组扁平化
const arr = [1, 2, 3, [5, 7, 8, [9, 10]], 0, 4];
// 扁平化一级:[1, 2, 3, 5, 7, 8, [9, 10], 0, 4]
function flatten(arr: any[]) {
let res = [];
for (let i = 0; i < arr.length; i++) {
res = res.concat(arr[i])
}
return res
}
// 深度扁平化:[1, 2, 3, 5, 7, 8, 9, 10, 0, 4]
function flatten(arr: any[]) {
let res: any[] = []
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
res = res.concat(Array.isArray(v) ? flatten(v) : v)
}
return res
}
curry
实现柯里化,原理主要是判断参数个数
const fn = (a: number, b: number, c: number, d: number) => {
return a + b + c + d
}
function curry(fn: Function) {
const len = fn.length;
let params = [];
function calc(...args: any[]) {
params = [...params, ...args].slice(0, len);
if (params.length === len) {
return fn(...params)
}
return calc
}
return calc
}
const fn2 = curry(fn);
console.log(fn2(1)(2)(3)(4))
深拷贝
Object.assign和展开符(...),是浅拷贝
const obj: any = {
a: 1,
b: { x: 10 },
c: [2, { d: 100 }],
e: Symbol('foo'),
fn: () => { console.log("fn...") },
map: new Map([
['name', '张三'],
['title', 'Author']
]),
set: new Set(["a", "b"])
}
obj.self = obj
obj.__proto__.f = 123
JSON.stringify,可以用来拷贝常见的数据。但是symbol、function:被忽略了,map、set:得到的都是空值,同时处理不了循环引用(会报错)
console.log("JSON", JSON.parse(JSON.stringify(obj)))
普通的拷贝方法,相比较json,可以处理symbol、function了,但同样map、set得到的还是空值,遇到循环引用也会报错
export function cloneDeep(target: any) {
if (typeof target !== "object" || target === null || target === undefined) {
return target
}
const res: any = target instanceof Array ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
res[key] = cloneDeep(target[key])
}
}
return res
}
解决循环引用:需要临时存储遍历过程中的值,又不能造成内存泄漏,选择WeakMap这种弱引用数据结构
export function cloneDeep(target: any, weakMap = new WeakMap()) {
if (typeof target !== "object" || target === null || target === undefined) {
return target
}
const map = weakMap.get(target)
if (map) {
return map
}
let res: any = {}
weakMap.set(target, res)
if (target instanceof Map) {
res = new Map()
target.forEach((v, k) => {
res.set(cloneDeep(k, weakMap), cloneDeep(v, weakMap))
})
} else if (target instanceof Set) {
res = new Set()
target.forEach(v => {
res.add(cloneDeep(v, weakMap))
})
} else if (target instanceof Array) {
res = target.map(v => cloneDeep(v, weakMap))
} else {
for (let i in target) {
res[i] = cloneDeep(target[i], weakMap)
}
}
return res
}
EventBus
// 自定义事件,发布订阅设计模式
export default class EventBus {
private events: {
[key: string]: Array<{ fn: Function; isOnce: Boolean }>
};
constructor() {
this.events = {}
}
on(key: string, fn: Function, isOnce: Boolean = false) {
if (!this.events[key]) {
this.events[key] = []
}
this.events[key].push({ fn, isOnce })
}
// 只触发一次,需要解绑
once(key: string, fn: Function) {
this.on(key, fn, true)
}
emit(key: string, ...args: any[]) {
if (this.events[key]) {
this.events[key] = this.events[key].filter(i => {
i.fn(...args);
return !i.isOnce
})
}
}
// 可以解绑某个函数,或者所有函数
off(key: string, fn?: Function) {
if (this.events[key]) {
if (!fn) {
delete this.events[key]
} else {
this.events[key] = this.events[key].filter(i => i.fn !== fn)
}
}
}
}
树与数组相互转换
数组的元素是有序排列的,只有这样才可以相互转换
数组转树
const flattenArr = [
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 }
]
// 数组元素
interface ArrItem {
id: number
name: string
parentId: number
}
// 节点元素,节点间是有层级的,不需要parentId
interface NodeItem {
id: number
name: string
children?: NodeItem[]
}
function fnArrToTree(arr: ArrItem[]): NodeItem | null {
const map: Map<number, NodeItem> = new Map()
let root: NodeItem | null = null
arr.forEach(item => {
const { id, name, parentId } = item
const curNode: NodeItem = { id, name }
// 将所有节点存起来,方便找到父节点
map.set(id, curNode)
const parentNode: NodeItem | undefined = map.get(parentId)
if (parentNode) {
if (!parentNode.children) {
parentNode.children = []
}
parentNode.children.push(curNode)
}
if (parentId === 0) {
root = curNode
}
})
return root
}
console.log(fnArrToTree(flattenArr))
树转数组
转换后的结果同上。难点在于:如何把同级节点遍历完,还按照当前节点顺序去遍历子节点,比如最终顺序是ABCDEF,而不是ABDECF,前者是广度优先遍历,后者是深度优先
const treeObj = {
id: 1,
name: '部门A',
children: [
{
id: 2,
name: '部门B',
children: [
{ id: 4, name: '部门D' },
{ id: 5, name: '部门E' }
]
},
{
id: 3,
name: '部门C',
children: [
{ id: 6, name: '部门F' }
]
}
]
}
interface ArrItem {
id: number
name: string
parentId: number
}
interface NodeItem {
id: number
name: string
children?: NodeItem[]
}
function fnTreeToArr(root: NodeItem): ArrItem[] {
// 使用栈来实现广度优先遍历
const stack: NodeItem[] = [root]
const res: ArrItem[] = []
// 保留节点和父节点的联系,方便查找parentId
const map: Map<NodeItem, NodeItem> = new Map()
while (stack.length) {
const cur: NodeItem | undefined = stack.shift()
if (cur) {
const { id, name } = cur
const parentNode = map.get(cur)
res.push({ id, name, parentId: parentNode ? parentNode.id : 0 })
if (cur.children) {
cur.children.forEach(i => {
map.set(i, cur)
stack.push(i)
});
}
}
}
return res
}
console.log(fnTreeToArr(treeObj))
lru算法
- 实现get、set。要够快复杂度 O(1)
- 处理超出溢出 (只缓存最近使用的, 删除沉水数据),实现超出溢出,就必须要有序的、可排序的
map
使用map实现,Map 的遍历顺序就是插入顺序(参考阮一峰es6)
- set:调用has/set两个api,复杂度O(1)
- get:has/get/delete等api,复杂度O(1)
- 处理超出溢出:调用map的keys方法(键名的遍历器),复杂度O(n)
export default class LRUMap {
private length: number
private mapData: Map<any, any> = new Map()
constructor(length: number) {
if (length < 1) {
throw new Error("invalid length")
}
this.length = length
}
// map的set,会有序的向后插入。访问可调用keys():迭代器
set(key: any, val: any) {
// 缓存最新:已有数据先删后加
if (this.mapData.has(key)) {
this.mapData.delete(key)
}
this.mapData.set(key, val)
// 处理溢出:使用map迭代器查找需要删除的key
if (this.mapData.size > this.length) {
// 超出,删掉前边的
const delKey = this.mapData.keys().next().value
this.mapData.delete(delKey)
}
}
get(key: any): any {
if (!this.mapData.has(key)) {
return null
}
// 已有数据同set,先删后插
const val = this.mapData.get(key)
this.mapData.delete(key)
this.mapData.set(key, val)
return val
}
}
链表
双向链表:满足有序,查询够快(头尾指针, 设置最新)
- set:只需更改next/prev,复杂度O(1)
- get:使用key记录每个节点,对象的查询复杂度 O(1)
- 处理超出溢出:需要从head遍历,复杂度最小O(1),最大O(n)
export default class LRULink {
private limit: number = 0
private length: number = 0
private head: LinkNode | null = null
private tail: LinkNode | null = null
private link: { [key: string]: LinkNode } = {}
constructor(limit: number) {
if (limit < 1) {
throw new Error("invalid limit")
}
this.limit = limit
}
// 处理长度溢出
private tryClean() {
while (this.length > this.limit) {
if (!this.head || !this.head.next) {
throw new Error("head or head.next is undefined")
}
const tamp = this.head.next
delete this.head.next
delete this.head.prev
delete this.link[this.head.key]
// 更新head和链表长度
this.head = tamp
this.length--
}
}
set(key: string | number, value: any) {
const curNode = this.link[key]
// 没有查到节点
if (!curNode) {
// 1.创建节点
const newNode: LinkNode = { key, value }
// 3.存到hash表中,更新链表长度
this.link[key] = newNode
this.length++
// 2.移动至末尾
this.moveToTail(newNode)
} else {
// 查到节点,更新值,移动至末尾
curNode.value = value
this.moveToTail(curNode)
}
// 处理长度溢出情况
this.tryClean()
}
// 将节点移动至末尾:设置为最新
private moveToTail(node: LinkNode) {
if (this.tail === node) {
return
}
// 1.切断node.prev、node.next和node联系
if (node.prev) {
if (node.next) {
node.prev.next = node.next
} else {
delete node.prev.next
}
}
if (node.next) {
if (node.prev) {
node.next.prev = node.prev
} else {
delete node.next.prev
}
}
// 2.建立node与head联系
if (this.length === 1) {
this.head = node
} else if (this.head === node && node.next) {
// 如果节点是头结点,将头结点更新为下一个节点
this.head = node.next
}
// 2.切断node.prev、node.next
delete node.prev
delete node.next
// 3.建立node与tail联系
if (this.tail) {
this.tail.next = node
node.prev = this.tail
}
this.tail = node
}
get(key: string | number): any {
const curNode = this.link[key]
if (!curNode) {
return null
}
// 节点是尾节点,直接返回
if (this.tail === curNode) {
return curNode.value
}
// 将当前节点,移动到末尾
this.moveToTail(curNode)
return curNode.value
}
}
变量提升
console.log("header.a", a) // function
var a = 0;
// 函数声明
function a() {}
console.log("end.a", a) // 0
console.log("header.b", b) // undefined
var b = 0;
// 函数表达式
var b = function () {}
console.log("end.b", b) // function
类型转换
条件判断时,基本数据类型转换
console.log(Boolean('')) // false
console.log(Boolean(-0)) // false
console.log(Boolean(-1)) // true
对象类型转换
// 1.先调用valueOf(),如果返回基本数据类型,就返回,否则下一步
// 2.调用toString(),比如数组,转成字符串,字符串是基本数据类型了,转换
// 3.[Symbol.toPrimitive] 优先级是最高的
var num = 1
var arr = [1, 2, 3];
var obj = {
a: 1
}
var primitive = {
a: 1,
valueOf() {
return 2
},
toString() {
return "3"
},
[Symbol.toPrimitive]() {
return 2
}
}
console.log(num + arr) // 11,2,3
console.log(num + obj) // 1[object Object]
console.log(num + primitive) // 3
四则运算
一方不是字符串或者数字,就将它转为字符串或者数字
true + true // 2
1 + [1, 2, 3] // 11,2,3
"a" + +"b" //aNaN +"b" = NaN
4 * [] // 0, [].valueOf = '', toString = 0
== 和 ===
// 如果一方时Boolean,就先把Boolean转为Number
"1" == true // "1" == 1 true
// 一方是object,另一方是Number或者String,先把object转为基本数据类型
"1,2,3" == [1, 2, 3] // true
"[object Object]" == {} //true
0.1 + 0.2 != 0.3 ?
计算机是使用二进制存储的,0.1在二进制中是无限循环的一堆数字,很多十进制的小数也都是无限循环的。然后js使用浮点数标准,会裁减掉一部分数字
这些无限循环的数字被裁减掉,就会出现精度丢失问题。就造成0.1不是0.1了,而是0.100000000000000002。同理,0.2也一样变成0.200000000000000002
0.1 + 0.2 === 0.30000000000000004 // true
为啥console.log(0.1), 输出0.1?这是因为再输入内容时,二进制又被转成了十进制,十进制转成了字符串。这个过程又是一个近似值,比如:
console.log(0.100000000000000002) // 0.1
解决方法:四舍五入下
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
闭包
闭包的定义:
- 可以访问函数内部变量的函数
- a函数包含b函数,b函数可以访问a函数内变量,b就是闭包
- 闭包就是 函数 + 函数内可访问变量的"和"
闭包的意义就是可以间接访问函数内部变量
// 异步,不用解释太多了
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
console.log("...");
// 每次循环,都执行一个函数,会产生一个独立的作用域,互不干扰
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i)
}, 0)
})(i)
}
// 不解释太多了,每次循环都会创建一个独立的块级作用域
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
// 定时器到期,就将传给第一个函数形参
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},i, i)
}
原型
function F(name) {
this.name = name
}
var f = new F("f")
console.log("f", f)
console.log("实例和构造函数的关系:----------")
console.log(f.__proto__ === F.prototype) //true
console.log(f.constructor === F) //true
console.log(F.prototype.constructor === F) //true
// 实例通过__proto__.constructor指向构造函数
// 构造函数通过prototype.constructor指向自身
console.log("为什么f有valueof、toString:----------")
console.log(Object.getOwnPropertyNames(Object.prototype))
// valueof、toString是Object原型上的方法, f的原型对象的原型,又指向了Object的原型
console.log(f.__proto__.__proto__ === Object.prototype) //true
console.log(Function.prototype.__proto__ === Object
.prototype) // true
console.log("为什么F有bind、call:----------")
console.log(Object.getOwnPropertyNames(Function.prototype))
// bind、call是Function原型上的方法, F的原型,又指向了Object的原型
console.log(F.__proto__ === Function.prototype) //true
// 那是不是可以理解为:函数都是Function的实例?
console.log(Function.__proto__ === Function.prototype) // true
// 连Function本身也是自己的实例...
console.log("其他内置对象又是从哪里来的:----------")
// Function、Object、Math、Date等构造函数,是否也都是Function的实例?毕竟他们都是内置的构造函数
console.log(Object.__proto__ === Function.prototype) // true
console.log(Array.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Math.__proto__ === Function.prototype) // true
console.log(Date.__proto__ === Function.prototype) // true
// 所有的js内置对象都是Function的实例?只有Math不是?
console.log("Function.prototype又是从哪里来的:----------")
console.log(Function.prototype.__proto__ === Object.prototype) //true
// Function的原型又指向了Object,Function的原型对象又是Object的实例?所以js的一切根源就是Object.prototype?
es6
var、let、const
var a = 0;
let b = 1;
const c = 2;
console.log("window.a", window.a) // 0
console.log("window.b", window.b) // undefined
console.log("window.c", window.c) // undefined
原型和calss继承
实现目标:
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function () {
console.log("name: ", this.name)
}
// 比较常用的,使用call来执行父的构造函数,但是实例的this指向子
function Child(name) {
Parent.call(this, name)
}
- 子实例有name属性
- 子继承父类的sayName方法
- 子实例的构造函数指向父类,并不影响父的实例
组合继承
以上只继承父类的属性,但是没有继承父类的sayName方法,实现的方法:
- 修改子构造函数为父的实例
- 修改子构造函数为父的原型
// 子构造函数的原型指向父类的实例,这样子的实例在自身找不到sayName,就去父类实例的原型上去找
Child.prototype = new Parent()
var child = new Child("child")
var child2 = new Parent("child2")
console.log("child", child)
console.log("child2", child2)
这样可以继承父类方法,但是有两个问题:
- 由于子类实例化后,原型指向了父实例,所以多了name属性
- 并且子实例的构造函数指向了父类
Child.prototype = Parent.prototype
var child = new Child("child")
console.log("child", child)
原型上没有父类的多余属性了,但是子类构造函数的指向,还是指向了父类。 所以要改变下子类构造函数的指向
Child.prototype.constructor = Child
但是又影响到了父类实例指向的构造函数
寄生组合继承
既能继承父的方法,又不影响子、父实例构造函数的指向。 使用Object.create,创建一个新对象,传入的参数将作为新对象的__proto__,并将新对象的构造函数指向子类
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child
}
})
完整代码:
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function () {
console.log("name: ", this.name)
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child
}
})
var child = new Child("child")
var child2 = new Parent("child2")
console.log("child", child)
console.log("child2", child2)
class
class Person {
constructor(name) {
this.name = name
}
sayName() {
console.log("name: ", this.name)
}
}
let person = new Person("person")
console.log("person", person)
class Child extends Person {
constructor(name) {
super(name)
}
}
let child = new Child("child")
console.log("child", child)
模块化
待补充...
手写代码
map
var arr = [{ a: 1}, { a: 2}, { a: 3}]
var a = arr.map(item => ({
a: item.a * 2
}))
console.log("a", a)
function _reduce(fn) {
var _this = this
return _this.reduce((a, b) => {
a.push(fn(b))
return a
}, [])
}
Array.prototype._reduce = _reduce
var b = arr._reduce(item => ({
a: item.a * 2
}))
console.log("b", b)
如果新函数被当做构造函数使用,那么实例化所传入的 this 将被忽略。
var f = new newFn()
console.log(f.__proto__ === newFn.prototype) //false
console.log(f.__proto__ === sayName.prototype) // true
console.log(f instanceof newFn, f instanceof sayName) // true, true
可以看到最终实例的原型不是新函数,而是原始函数,相当于f = new sayName(),最后可以看到,新函数和原函数都在实例的原型链上。
- 调用bind(),返回新函数,并将参数作为this,就是闭包函数
- 调用新函数,就是把原函数当做第一个参数的方法调用,apply再合适不过
- 收集除了第一个参数外的参数,都传入原始函数
- 当新函数作为构造函数使用时候,新函数和原函数都在实例的原型链上,但实例的原型还是指向原始函数的原型对象
function _bind() {
var fn = this
var args = [...arguments]
var _this = args.shift()
function _newFn() {
let _args = [...args, ...arguments]
return fn.apply(this instanceof _newFn ? this : _this, _args)
}
function _fn() {
}
_fn.prototype = this.prototype
_newFn.prototype = new _fn()
return _newFn
}
Function.prototype._bind = _bind
排序
- 冒泡排序
var arr = [6, 1, 40, 3, 9, 2, 50, 64, 0]
let _start = performance.now();
for (var i = 0; i < arr.length; i++) {
for (var j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
let tamp = arr[i]
arr[i] = arr[j]
arr[j] = tamp
}
}
}
let _end = performance.now()
console.log("arr", _end - _start, arr);
- 快速排序
var arr2 = [6, 1, 40, 3, 9, 2, 50, 64, 0]
function test(arr) {
if (!arr.length) {
return arr
}
let left = []
let right = []
let middle = [arr.shift()]
arr.forEach(item => {
if (item > middle[0]) {
right.push(item)
} else {
left.push(item)
}
})
return test(left).concat(middle, test(right))
// return [...test(left), ...middle, ...test(right)]
}
let _start2 = performance.now();
let arr3 = test(arr2)
let _end2 = performance.now()
console.log("arr3", _end2 - _start2, arr3);
设计模式
工厂模式
作用就是隐藏创建实例的复杂度,只提供一个接口使用,让使用者感到简单、清晰
class Person {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
class Factory {
static create(name) {
return new Person(name)
}
}
Factory.create('kaixin').sayName()
单例模式
全局状态等往往只需要一个对象,就可以使用单例模式,全局有且只有一个这样的对象
class Person {
constructor(name) {
this.name = name
this.instance = null
}
static getInstance(name) {
return this.instance ? this.instance = new Person(name) : this.instance
}
}
let person1 = Person.getInstance()
let person2 = Person.getInstance()
console.log(person1 === person2);
垃圾回收
- 内存泄漏的定义:程序不能释放已经不再使用的内存,造成内存浪费,使得程序运行缓慢,甚至崩溃
- 垃圾回收器(GC)原理:标记清除法。window等的根节点占用的内存不会被回收,从根节点出发,递归检查子节点,都标记为引用状态,不会被回收。没有被标记的内存块,被视为垃圾内存,要被回收,从新分配资源
常见的几种内存泄漏:
- xxx = window.xxx,解决方法:使用var、let、const。或者使用完置为null
- 定时器、闭包
- 循环引用,旧的计数算法会内存泄漏,标记清除不会
var a=new Object;
var b=new Object;
a.r=b;
b.r=a;
- dom的引用
var select = document.querySelector;
var treeRef = select('#tree');
var leafRef = select('#leaf'); //在DOM树中leafRef是treeFre的一个子结点
select('body').removeChild(treeRef); //#tree不能被回收入,因为treeRef还在