Javascript

77 阅读8分钟

基础

  • 内置类型
六个基本类型 ***null******undefined*****boolean****number****string****symbol**
一个引用类型 **Object**

number类型是浮点类型,没有整数,NaN也属于number类型,并且NaN不等于自身
  • symbol 介绍
# 唯一性
let s1 = Symbol();
let s2 = Symbol();
# a就是对当前创建得symbol类型得修饰
let s3 = Symbol('a');
let s4 = Symbol('a');
s1 !== s2
s3 !== s4

# 只能通过显式得方式转为字符串或布尔值
String(Symbol())
Boolean(Symbol())

# 作为对象得属性
let mySymbol = Symbol();
let a = {};
Object.defineProperty(a, mySymbol, { value: 1 })

# 对象属性得遍历
# Symbol作为属性名,forIn、forOf、Object.keys、Object.getOwnPropertyNames、JSON.stringify 都无法输出出来
# 1、可以用Object.getOwnPropertySymbols获取指定对象的所有Symbol属性名
# 2、可以用Reflect.ownKeys获取对象所有属性,字符串属性 + Symbol属性,Reflect是ES6推出相当于Object得方法集容器,利于管理,除了枚举外得十三种对象内键操作方法都有
let s1 = Symbol.for('a');
let s2 = Symbol.for('a');
s1 === s2
# Symbol.for('a')的创建方式会在创建之前在全局中寻找,有没有用Symbol.for()的方式,并且key是'a'的字符串创建了Symbol类型(创建了就会在全局中登记),如果有则不重复创建,直接用已创建的(已登记的)
# 而Symbol('a')的创建是不会去检索全局的,是直接创建一个新的Symbol类型。

# Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key,变量s2属于未登记的 Symbol 值。
# Symbol.keyFor()这个方法,主要服务于Symbol.for()。
let s1 = Symbol.for('a');
Symbol.keyFor(s1); // 'a'
let s2 = Symbol('a');
Symbol.keyFor(s2); // undefined
  • typeof
typeof 对于基本类型,除了null都可以显示正确得类型
typeof 1            // 'number'
typeof '1'          // 'string'
typeof undefined    // 'undefined'
typeof true         // 'boolean'
typeof Symbol()     // 'symbol'
typeof b            // b 没有声明,但是还会显示 undefined

typeof null         // 'object'     
# 因为在JS的最初版本中,使用的是32位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来

# typeof 对于对象,除了函数都会显示object
typeof []           // 'object'
typeof {}           // 'object'
typeof console.log  // 'function'

# 如果我们想正确得获取一个变量得类型,可以通过 Object.prototype.toString.call(xx)。获得类似 [object Type] 得字符串
Object.prototype.toString.call(1)
'[object Number]'
Object.prototype.toString.call('1')
'[object String]'
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(Symbol('a'))
'[object Symbol]'
Object.prototype.toString.call([])
'[object Array]'
Object.prototype.toString.call({})
'[object Object]'
Object.prototype.toString.call(console.log)
'[object Function]'
  • instanceof
# instanceof可以正确判断对象得类型,因为内部机制是通过判断对象得原型链中是不是能找到类型得prototype

function instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}
  • 类型转换
# 转Boolean,除了undefinednullfalseNaN0、-0''、其他的所有值都转为true,包括对象

# 对象转基本类型,会先调用valueOf,然后调用toString。并且这两个方法可以重写
# 也可以重写 [Symbol.toPrimitive] () {},该方法在转基本类型时调用优先级最高
  • 四则运算符
# 加法一方字符串,另一方隐式转换为字符串,其他运算一方是数字,那么另一方隐式转换为数字。
# 并且加法会触发三种类型转换:值转换为原始值、转换为数字Number(x)、转换为字符串。
1 + '1'             // '11'
2 * '2'             // 4

[1, 2] + [2, 1]     // '1,22,1'
# [1, 2].toString() -> '1,2'
# [2, 1].toString() -> '2,1'
# '1,2' + '2,1' = '1,22,1'

'a' + + 'b'         // "aNaN"
+ 'b'               // NaN
+ '1'               // 1
  • == 操作符
Type(x)与Type(y)相同,则
    为Undefinednull返回trueNumber,有NaN就返回false,-0==0,比较值
    为String,完全相同字符序列返回trueBoolean,同为true或者false返回true
    为对象,看是否是引用的同一个
不同,则
    null == undefined
    一个为Number,另一个为StringStringNumber比较
    有一个为BooleanBooleanNumber比较
    一个为NumberString,另一个为ObjectObject转基本类型比较
  • 比较运算符
# 字符串通过unicode字符索引比较
# 对象通过toPrimitive转换为基本类型比较
  • 原型
# 在ES标准中是 [[Prototype]],谷歌浏览器实现就是将它命名为__proto__
# __proto__ 和 constructor属性是对象所独有的
# prototype是函数所独有的,所以函数有 prototype 和 __proto__ 和 constructor

# __proto__由一个对象指向他的原型对象,当访问一个对象的属性如果不存在的时候,就会去__proto__指向的对象里找,一直往上,最后null为终点,即原型链
# prototype含义是函数的原型对象,也是这个函数所创建的实例的原型对象,让所有实例化的对象们都能找到公用的属性和方法,任何函数在创建的时候默认同时创建该函数的prototype对象
# constructor构造函数,重点就是Function这个函数
  • 继承
# ES5实现思路是将子类原型设置为父类的原型
function Super() {}
Super.prototype.getNumber = function() {
    return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
    constructor: {
        value: Sub,
        enumerable: false,
        writable: true,
        configurable: true
    }
})

# ES6使用class语法
class MyDate extends Date {
  test() {
    return this.getTime()
  }
}
let myDate = new MyDate()
myDate.test()

# 报错因为ES6不是所有浏览器都兼容,需要使用Babel编译,而且JS底层有限制,如果不是由Date构造出来的实例,不能调用Date的函数,侧面说出ES6class继承和ES5的继承不同
# 改变思路实现继承
function MyData() {}
MyData.prototype.test = function () {
  return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(MyData.prototype, Date.prototype)
Object.setPrototypeOf(d, MyData.prototype)
  • new
# 1、函数被调用
# 2、新生成了一个对象
# 3、链接到原型
# 4、绑定 this
# 5、返回新对象的引用

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}
  • this
# 严格模式下是undefined
# call(绑定对象, 参1, 参2 ...)
# apply(绑定对象, [参1, 参2 ...])
# bind(绑定对象, 参1, 参2 ...)
# bind创建新对象永久绑定。

function foo() {
    console.log(this.a)
}
var a = 1
foo()               // this指向全局window

var obj = {
    a: 2,
    foo: foo
}
obj.foo()           // this指向obj

var c = new foo()   // this绑定在c上
c.a = 3
console.log(c.a)

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())    
# 箭头函数没有this,取决于外面的第一个不是箭头函数得this,指向window
  • call apply bind
# call、apply临时改变this的指向,bind创建一个新对象永久绑定
# call传参数列表,apply传参数数组,bind传参数列表

Function.prototype.myCall = function (context) {
    var context = context || window;
    # 给context添加一个属性
    # getValue.myCall(a, 'yck', '24') => a.fn = getValue
    # 这里 this 指向 getValue 这个fn
    context.fn = this;
    # 将 context 后面的参数取出来
    var args = [...arguments].slice(1);
    # getValue.call(a, 'yck', '24') => a.fn('yck', '24')
    var result = context.fn(...args);
    # 删除 fn
    delete context.fn;
    return result;
}

Function.prototype.myApply = function (context) {
    var context = context || window;
    context.fn = this;

    var result;
    # 需要判断是否存在第二个参数
    # 如果存在,就将第二个参数展开
    if (arguments[1]) {
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }

    delete context.fn;
    return result;
}

Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error');
    }
    var _this = this;
    var args = [...arguments].slice(1);
    # 返回一个函数
    return function F() {
        # 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
    }
}
  • 执行上下文
# 当执行JS代码时,会产生三种执行上下文
    全局执行上下文
    函数执行上下文
    eval执行上下文

# 每个执行上下文都有三个重要属性
    变量对象(VO),包含变量、函数声明和函数得形参,该属性只能在全局上下文中访问
    作用域链
    this

var a = 10
function foo(i) {
  var b = 20
}
foo()

# 上述代码,执行栈中有两个上下文:全局上下文和函数foo上下文
    stack = [
        globalContext,
        fooContext
    ]

# 其中全局上下文VO
    globalContext.VO === globe
    globalContext.VO = {
        a: undefined,
        foo: <Function>
    }

# 对于函数foo来说,VO不能访问,只能访问到活动对象(AO)
    fooContext.VO === foo.AO
    fooContext.AO = {
        i: undefined,
        b: undefined,
        arguments: <>   
        # arguments是函数独有的对象(箭头函数没有),伪数组,该对象中得callee属性代表函数本身,caller属性代表函数得调用者
    }

# 对于作用域链,可以理解成包含自身变量和上级变量对象得列表,通过 [[Scope]] 属性查找上级变量
    fooContext.[[Scope]] = [
        globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
        globalContext.VO,
        fooContext.VO
    ]

# 函数和变量提升得原因:将声明的代码移动到了顶部。更准确的解释是生成执行上下文得时候有两个阶段:
    创建阶段。创建VO,JS解释器会找出需要提升得变量和函数,提前在内存中开辟好空间,函数是将整个函数存入内存中,变量是只声明赋值undefined
    执行阶段。
# let也会提升,但是因为临时死区导致不能再声明前使用。


# 经典面试题,循环中定时器输出i
for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
# 因为setTimeout是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

# 方法一,匿名函数写法 (function(){}()) 或者 (function(){})() 
    for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function timer() {
                console.log(j);
            }, j * 1000);
        })(i);
    }

# 方法二,setTimeout第三个及后面的都是里面第一个参数(函数)的入参
    for ( var i=1; i<=5; i++) {
        setTimeout( function timer(j) {
            console.log( j );
        }, i*1000, i);
    }

# 方法三,使用let,块级作用域,相当于:
    { // 形成块级作用域
        let i = 0
        {
            let ii = i
            setTimeout( function timer() {
                console.log( ii );
            }, i*1000 );
        }
        i++
        {
            let ii = i
        }
        i++
        {
            let ii = i
        }
        ...
    }
  • 深浅拷贝
# 对象是保存的引用地址
# 浅拷贝可以通过a = Object.assign({}, b) 把b可枚举的属性不包括继承的替换第一个参数、扩展运算符来解决。
# 但是遇到层级比较深或者对象套对象,就需要深拷贝JSON.parse(JSON.stringify(a)),但是有局限性:
    会忽略undefined
    会忽略Symbol
    不能序列化函数
    不能解决循环引用的对象
# 建议深拷贝使用lodash的深拷贝函数、a = Object.create(b)原型链继承或自己封装。

function checkType(any) {
    return Object.prototype.toString.call(any).slice(8, -1)
}
function clone(any){
    if(checkType(any) === 'Object') { // 拷贝对象
        let o = {};
        for(let key in any) {
            o[key] = clone(any[key])
        }
        return o;
    } else if(checkType(any) === 'Array') { // 拷贝数组
        var arr = []
        for(let i = 0, leng = any.length; i<leng; i++) {
            arr[i] = clone(any[i])
        }
        return arr;
    } else if(checkType(any) === 'Function') { // 拷贝函数
        return new Function('return '+any.toString()).call(this);
    } else if(checkType(any) === 'Date') { // 拷贝日期
        return new Date(any.valueOf());
    } else if(checkType(any) === 'RegExp') { // 拷贝正则
        return new RegExp(any);
    } else if(checkType(any) === 'Map') { // 拷贝Map 集合
        let m = new Map()
        any.forEach((v,k)=>{
            m.set(k, clone(v))
        })
        return m;
    } else if(checkType(any) === 'Set') { // 拷贝Set 集合
        let s = new Set()
        for(let val of any.values()) {
            s.add(clone(val))
        }
        return s;
    }
    return any;
}
  • AMD
AMD是由RequireJS提出的。
define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
})
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
})
  • CommonJS
# CommonJSNode独有的规范,浏览器使用需要用到Browserify解析。
module.exports = { 
    a: 1
}
exports.a = 1
# 底层是 exports = module.exports,所以exports只能给属性添加值,不能直接赋值

var module = require('./a.js')

# CommonJSES6模块化区别
    CommonJS支持动态导入,require(${path}/xx.js)
    CommonJS用于服务器,同步。ES6模块化用于浏览器,异步。
    CommonJS导出是值拷贝,导出值改变了,导入值不会改变。ES6是导入导出值指向同一个内存地址,所以导入值会跟着导出值变化。
    ES6模块化会编译成require/exports来执行。
  • ES6模块化
export function a () {}
export const b = 555

# 只能有一个export default,引入可取名
export default function () {}

# 可用as重命名
import {a as a1, b} from './test.js'
import xxx from './test.js'
  • V8下得垃圾回收机制
# V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。

新生代算法
    新生代中得对象一般存活时间较短,使用Scavenge GC算法
    内存空间分为From和To空间,必定有一个空间是使用一个是闲置得,新分配的对象放入From空间,当From被占满时,新生代GC启动,算法检查From空间存活的复制到To空间,失活得销毁,From和To空间互换。

老生代算法
    老生代中得对象一般存活时间较长且数量多,使用两个算法:标记清除和标记压缩
    什么情况下对象会出现在老生代空间:
        新生代中对象经历过一次Scavenge算法,就会从新生代空间移到老生代空间
        To空间得对象占比超过25%,会将这个对象移到老生代空间

老生代空间很复杂,有如下几个空间
    enum AllocationSpace {
        // TODO(v8:7464): Actually map this space's memory as read-only.
        RO_SPACE,    // 不变的对象空间
        NEW_SPACE,   // 新生代用于 GC 复制算法的空间
        OLD_SPACE,   // 老生代常驻对象空间
        CODE_SPACE,  // 老生代代码对象空间
        MAP_SPACE,   // 老生代 map 对象
        LO_SPACE,    // 老生代大空间对象
        NEW_LO_SPACE,  // 新生代大空间对象

        FIRST_SPACE = RO_SPACE,
        LAST_SPACE = NEW_LO_SPACE,
        FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
        LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
    };

在老生代中,以下情况会先启动标记清除算法:
    某一个空间没有分块的时候
    空间中被对象超过一定限制
    空间不能保证新生代中的对象移动到老生代中

在这个阶段中,会遍历堆中所有的对象,标记活的对象,销毁所有没有被标记的对象。在标记大型堆内存时,可能需要几百毫秒才能完成一次。会导致一些性能上的问题。
为了解决这个问题:
    2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,标记分解为更小的模块,可以让 JS 在模块间隙执行,从而不至于让应用出现停顿情况。
    2018 年,改为并发标记。可以让 GC 扫描和标记对象时,同时允许 JS 运行。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

提升

  • Promise实现
# 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

# promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
    let _this = this;
    _this.currentState = PENDING;
    _this.value = undefined;
    # 用于保存 then 中的回调,只有当 promise 状态为 pending 时才会缓存,并且每个实例至多缓存一个
    _this.resolvedCallbacks = [];
    _this.rejectedCallbacks = [];

    _this.resolve = function (value) {
        if (value instanceof MyPromise) {
            # 如果 value 是个 Promise,递归执行
            return value.then(_this.resolve, _this.reject)
        }
        setTimeout(() => { // 异步执行,保证执行顺序
            if (_this.currentState === PENDING) {
                _this.currentState = RESOLVED;
                _this.value = value;
                _this.resolvedCallbacks.forEach(cb => cb());
            }
        })
    };

    _this.reject = function (reason) {
        setTimeout(() => { // 异步执行,保证执行顺序
            if (_this.currentState === PENDING) {
                _this.currentState = REJECTED;
                _this.value = reason;
                _this.rejectedCallbacks.forEach(cb => cb());
            }
        })
    }
    # 用于解决 new Promise(() => throw Error('error))
    try {
        fn(_this.resolve, _this.reject);
    } catch (e) {
        _this.reject(e);
    }
}


MyPromise.prototype.then = function (onResolved, onRejected) {
    var self = this;
    // 规范 2.2.7,then 必须返回一个新的 promise
    var promise2;
    // 规范 2.2.onResolved 和 onRejected 都为可选参数
    // 如果类型不是函数需要忽略,同时也实现了透传 Promise.resolve(4).then().then((value) =>       console.log(value))
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

    if (self.currentState === RESOLVED) {
        return (promise2 = new MyPromise(function (resolve, reject) {
        // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
        // 所以用了 setTimeout 包裹下
            setTimeout(function () {
                try {
                    var x = onResolved(self.value);
                    resolutionProcedure(promise2, x, resolve, reject);
                } catch (reason) {
                    reject(reason);
                }
            });
        }));
    }

    if (self.currentState === REJECTED) {
        return (promise2 = new MyPromise(function (resolve, reject) {
            setTimeout(function () {
                // 异步执行onRejected
                try {
                    var x = onRejected(self.value);
                    resolutionProcedure(promise2, x, resolve, reject);
                } catch (reason) {
                    reject(reason);
                }
            });
        }));
    }

    if (self.currentState === PENDING) {
        return (promise2 = new MyPromise(function (resolve, reject) {
            self.resolvedCallbacks.push(function () {
                // 考虑到可能会有报错,所以使用 try/catch 包裹
                try {
                    var x = onResolved(self.value);
                    resolutionProcedure(promise2, x, resolve, reject);
                } catch (r) {
                    reject(r);
                }
            });

            self.rejectedCallbacks.push(function () {
                try {
                    var x = onRejected(self.value);
                    resolutionProcedure(promise2, x, resolve, reject);
                } catch (r) {
                    reject(r);
                }
            });
        }));
    }
};



function resolutionProcedure(promise2, x, resolve, reject) {
    // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
    if (promise2 === x) {
        return reject(new TypeError("Error"));
    }
    // 规范 2.3.2
    // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
    if (x instanceof MyPromise) {
        if (x.currentState === PENDING) {
            x.then(function (value) {
                // 再次调用该函数是为了确认 x resolve 的
                // 参数是什么类型,如果是基本类型就再次 resolve
                // 把值传给下个 then
                resolutionProcedure(promise2, value, resolve, reject);
            }, reject);
        } else {
            x.then(resolve, reject);
        }
        return;
    }
    // 规范 2.3.3.3.3
    // reject 或者 resolve 其中一个执行过得话,忽略其他的
    let called = false;
    // 规范 2.3.3,判断 x 是否为对象或者函数
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
        // 规范 2.3.3.2,如果不能取出 then,就 reject
        try {
            // 规范 2.3.3.1
            let then = x.then;
            // 如果 then 是函数,调用 x.then
            if (typeof then === "function") {
                // 规范 2.3.3.3
                then.call(
                    x,
                    y => {
                        if (called) return;
                        called = true;
                        // 规范 2.3.3.3.1
                        resolutionProcedure(promise2, y, resolve, reject);
                    },
                    e => {
                        if (called) return;
                        called = true;
                        reject(e);
                    }
                );
            } else {
                // 规范 2.3.3.4
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        // 规范 2.3.4,x 为基本类型
        resolve(x);
    }
}
  • Generator实现
# 使用 * 表示这是一个 Generator 函数
# 内部可以通过 yield 暂停代码
# 通过调用 next 恢复执行,value是yield后面的返回值
function* test() {
    let a = 1 + 2;
    yield 2;
    yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }
  • async await
# 是Generator函数的语法糖
# async 声明一个异步函数,返回Promise对象
# await只能在async函数中使用,暂停异步得功能执行,后面需要一个Promise对象如果不是则会被转成Promise对象,只要有一个Promise对象reject了,整个async函数都会中断,如果resolve,返回值就是.then得参数。
# 最好把await放到tryCatch中

# 如果发出三个互不依赖得请求可以
let results = await Promise.all([getA, getB, getC])
  • Map、FlatMap 和 Reduce
# Map 作用是生成一个新数组,遍历原数组,三个参数,分别是当前索引元素,索引,原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。
[1, 2, 3].map((v) => v + 1)
// -> [2, 3, 4]

# FlatMap 和 map 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。
[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

arr.reduce(function (prev, cur, index, arr) {
    // reduce第一个参数是个回调函数,循环遍历数组元素然后用这个回调函数处理
    // prev上次返回得或者是第一个或者是默认设置值
    // initialValue 初始默认值 如果设置循环就从第0开始,如果不设置就直接从第1开始prev是第0个值
    return prev + cur
},[initialValue])
  • Proxy
# 给对象设置代理,通过修改原对象代理对象也会相应改变
let onWatch = (obj, setBind, getLogger) => {
    let handler = {
        get(target, property, receiver) {
            getLogger(target, property)
            return Reflect.get(target, property, receiver);
        },
        set(target, property, value, receiver) {
            setBind(value);
            return Reflect.set(target, property, value);
        }
    };
    return new Proxy(obj, handler);
};

let obj = { a: 1 };
let value;
let p = onWatch(
    obj, 
    (v) => {
        value = v
    }, 
    (target, property) => {
        console.log(`Get '${property}' = ${target[property]}`);
    }
)
p.a = 2 // value: 2、 obj = {a: 2}
p.a // -> Get 'a' = 2
  • 防抖节流
# 防抖和节流本质是不一样的。
# 防抖 是每次执行一个定时器下一次的将上一次的清空,只执行最后一次。
# 节流 是记录上次执行时间判断这次执行时间差是否大于设定的时间差,决定是否执行
  • rem.js
# 方案一
!function(n){
    var  e=n.document,
         t=e.documentElement,
         i=720,
         // 设计稿的宽度/rem换算比例
         d=i/100,
         o="orientationchange"in n?"orientationchange":"resize",
         a=function(){
             var n=t.clientWidth||320;n>720&&(n=720);
             // 改变根节点html的大小
             t.style.fontSize=n/d+"px"
         };
         e.addEventListener&&(n.addEventListener(o,a,!1),e.addEventListener("DOMContentLoaded",a,!1))
}(window);


# 方案二
window.onload = function(){
    /*  
        720设计稿的宽度 
        100换算比例,比如宽度是100px,就可以写为1rem
    */
    getRem(720,100)
};
window.onresize = function(){
    getRem(720,100)
};
function getRem(pwidth,prem){
    var html = document.getElementsByTagName("html")[0];
    var oWidth = document.body.clientWidth || document.documentElement.clientWidth;
    html.style.fontSize = oWidth/pwidth*prem + "px";
}
  • 为什么 0.1 + 0.2 != 0.3
# 因为JS采用IEEE754双精度版本(64位),所有采用IEEE754都有该问题
# 计算机表示十进制是采用二进制

parseFloat((0.1 + 0.2).toFixed(10))