javascript相关知识

330 阅读15分钟

欢迎访问我的博客

关于this指向问题

  • 默认是全局对象, 严格模式下为 undefined
  • 普通函数的 this 在调用时确定
    • 谁调用, 谁就是this.
    • 作为对象方法, 原型链方法, 对象的 getter/setter 方法时也是如此.
    • 使用 bind call apply 可绑定 this. 替代调用时确定的规则.
  • 箭头函数的this继承自作用域链上层
  • new 构造函数, this为正在构造的对象
  • dom 事件处理函数, this为触发事件的元素.
  • on-event 内联函数, this 为绑定的元素.

具体场景

const log = () => console.log.apply(console, arguments)

global.a = 'global'

const test = {
    a: 'test',
    b: function() {
        log('this.a', this.a)
    }
}

const that = test.b
test.b() // test 谁调用它this指向谁
that.apply(test) // 'test' 绑定this
that() // global 默认全局

let btn = document.createElement('button')
document.querySelector('body')
        .appendChild(btn.innerText = 'click')

btn.addEventListener('click', function (event) {
    // dom 事件处理函数, this为触发事件的元素.
    log('this test 5', this)
})

btn.click()

关于call, bind, apply的实现

const log = console.log.bind(console)

function Person() {
    this.name = 'woyao'
    this.getName = function(...args){
        return this.name + args.toString()
    }
}

name = 'tomas'
const person = new Person()
const getNameFunc = person.getName
// func.call(person, ...args)
// this => context, func => this获得
Function.prototype.Mycall = function(context, ...args) {
    context = context || window
    // this => 谁调用this就指向谁, 所以这里通过this拿到func
    context.func = this
    const result = context.func(...args)
    delete context.func
    return result
}

// func.apply(obj, [...args])
Function.prototype.Myapply = function(context, args) {
    context = context || window
    context.func = this
    const result = context.func(...args)
    delete context.func
    return result
}

// const bindFunc = func.bind(obj, ...args), bindFunc()
Function.prototype.Mybind = function(context, ...args) {
    context = context || window
    context.func = this
    const result = () => {
        const res = context.func(...args)
        delete context.func
        return res
    }
    return result
}

// log(getNameFunc())
// log(getNameFunc.Mycall(person, 'chen', 'xiao'))

关于屏幕的适配

媒体查询

@meida screen and (max-wdith: 360px) {
    html {
        font-size: 20px;
    }
}

动态修改(rem)

在国内一般都是用的rem规范, 默认是1rem指根元素fontSize的值. 国外用的一般em, em是指父元素的fontSize. 当然也有用vm, vh的。

function setBaseFont() {
    let htmlWidth = document.documentElement.clientWidth  || document.body.clientWidth;
    let htmlDom = document.querySelector('html);
    // (htmlWidth / 375) 是默认以iphone6机型的屏幕尺寸为基准
    // 一般业内的准则是设置1rem = 屏幕尺寸 / 10
    // 现在是以750尺寸的iphone6机型为准, 那么1rem = 75
    htmlDom.style.fontSize = (htmlWidth / 375) * 75 + 'px';
}

window.addEventListener('resize', setBaseFont)
window.addEventListener('pageshow', setBaseFont)

setBaseFont()

然后有两中办法使用,一种是用css把所有的px变成rem。 另一种是插件在vscode中下载px2rem插件自动转成rem 当然如果是一个模块化工程中,可以通过设置webpack引用的postcss-plugin-px2rem插件来解决

  • 通过使用css
@function px2rem($px) {
    $rem: 37.5;
    @return ($px / $rem) + rem;
}
  • 通过使用webpack
{
    loader: require.resolve("postcss-loader"),
    options: {
        postcssOptions: {
            plugins: [
                require("postcss-preset-env")({
                    autoprefixer: {
                        overrideBrowderslist: "andoroid >= 4.3",
                    }, // 添加webkit, mozilla前缀
                    stage: 3,
                }),
                require("postcss-plugin-px2rem")({
                    rootValue: 75, // 根据index.html的doucment fontsize设置
                    minPixelValue: 2, // 设置要替换的最小像素值
                }),
            ],
        },
    },
},

自定义事件

  • 原生javascript自带的Event类或者CustomEvent实现
let eve = new Event('custom')
element.addEventListener('custom', function(){ console.log("custom") }
element.dispatchEvent(eve)
  • 自己编写EventEmitter类实现
class EventEmitter() {
  constructor() {
    this.events = {}
  }
  on(event, callback) {
    (this.events[event] || (this.events[event] = [])).push(callback)
  }
  emit(event, ...args) {
    (this.events[name] || []).forEach(fn => fn(...args))
  }
  once(event, callback) {
    const wrap = (...args) => {
      callback.apply(this, args)
      this.off(event, wrap)
    }
		this.on(event, wrap)
  }
  off(event, callback) {
    this.events[name]
        && this.events[event].splice(this.events[event].indexof(callback) >>> 0, 1)
  }
}

各种http请求方式

  • fetch的方式请求数据,

    • fetch 不支持同步请求
    • fetch 不支持取消一个请求
    • fetch 无法查看请求的进度
    • fetch 只有遇到网络错误的时候才会reject这个promise请求 对于options、跨域等错误响应,Promise也是resolved状态,需要用response.ok来进行判断

    用法如下:

    fetch('/api/user.json?id=2', {
      credentials: 'include' // 携带cookie信息
    })
       .then((response) => {
    		// 返回的response是一个ReadableStream对象,需要调用对象的json方法进行格式化
        if(!response.ok) Promise.reject('cannot get data')
       	return response.json();
       })
       .then(data => {
         console.log(data);
       })
    	 .catch(err => console.log(err))
    
  • ajax ajax的readState有五种状态:

    • 0(未初始化)还没有调用open()方法
    • 1(载入)。已经调用 open()方法,但尚未调用 send()方法
    • 2(载入完成)send()方法执行完成,
    • 3(交互)正在解析响应内容
    • 4(完成)响应内容解析完成,可以在客户端调用了
const ajax = function (method, path, headers, data, runCallBack) {
    var xhr = new XMLHttpRequest()
    xhr.open(method, path, true)
    Object.entries(headers).forEach(([header, value]) => {
        xhr.setRequestHeader(header, value)
    })
    xhr.onprogress = (event) => {}
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            runCallBack(xhr.response)
        }
    }
    xhr.send(data)
}
  • 关于axios用法 axios 是一个流行的http请求库
    • 默认axios会把要发送的JavaScript对象数据序列化为JSON
    • 支持请求拦截和响应拦截
    • axios.all 方法支持并发
    • axios.defaults 可以全局配置默认值
    • axios.CancelToken实例支持取消请求
    • 可以查看请求进度 用法如下:
      import axios from 'axios'
      import qs from 'qs'
    
      const source = axios.CancelToken.source()
    
      const baseConfig = {
          timeout: '2000',
          // baseURL将自动加在url前面, 除非url是一个绝对URL。
          baseURL: 'https://some-domain.com/api/',
          // 上传处理进度事件
          onUploadProgress: function (progressEvent) {
              console.log(`上传进度${progressEvent.loaded / progressEvent.total * 100}%`)
          },
          // 下载处理进度事件
          onDownloadProgress: function (progressEvent) {
              console.log(`下载进度${progressEvent.loaded / progressEvent.total * 100}%`)
          },
          cancelToken: source.token
      }
      const options = {
          headers: {
              'Content-Type': 'application/x-www-form-urlencode'
          },
          url: '/user/all',
          method: 'post',
          withCredentials: false, // 跨域请求时是否需要使用凭证(Cookie)
          data: {
              username: 'woyao'
          },
      }
    
      const createRequest = (baseConfig, options) => {
          // 使用create创建一个axios实例
          const instance = axios.create(baseConfig)
    
          instance.interceptors.request.use(config => {
              // 在发送请求之前做些什么, 比如设置一个全局的loading组件显示
              switch (config.headers && config.headers['Content-Type']) {
                  case 'application/x-www-form-urlencoded':
                      // 这里也可以自己实现一个serialize函数
                      config.data = qs.stringify(config.data)
                      break
                  case 'multipart/form-data':
                      if (data instanceof HTMLFormElement) {
                          data = new FormData(data)
                      } else {
                          Promise.reject('data type is not valid of your ContentType defined')
                      }
                      break
                  case 'application/json':
                      break
                  default:
                      break
              }
              return config
          }, error => {
              // 对请求错误做些什么
              return Promise.reject(error)
          })
          instance.interceptors.response.use((response) => {
              // 对返回的数据进行拦截处理
              // 比如关闭一个全局的loading组件
              return response
          }, (error) => {
              // Do something with response error
              return Promise.reject(error)
          })
          return instance(options)
      }
    
      createRequest(options).then().catch()
    
      // 手动取消请求
      // source.cancel('Operation canceled by the user.')
    

EventLoop事件机制

主线程(javaScript引擎线程)从"任务队列"中读取事件,这个过程是循环不断的, 所以整个的这种运行机制又称为Event Loop(事件循环)。

讲白了这个机制是因为js单线程的原因。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

当事件触发线程执行的时候,此时cpu空闲,如果非要等事件触发线程从阻塞状态变成就绪状态肯定是不行的。我们应该要用并发机制。在一个时间片段中多个线程可以交替执行。尽可能的是cpu跑起来。EventLoop就是一种并发机制策略。

在任务队列中: 任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

EventLoop:

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件发生,找出事件对应的异步任务放入执行栈,主线程结束等待状态,开始执行。
  4. 主线程不断重复上面的第三步

注意: IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程在读取"任务队列"就是读事件,从而知道哪些异步任务可以放入执行栈。另外如果一个事件被添加了定时器,只有到了规定的时间才能返回主线程

另外既然扯到了EventLoop就不得说说Process.nextTicksetImmediatesetTimeoutrequestAnimationFrame

  • Process.nextTick(() => {})

在当前"执行栈"的尾部到下一次主线程读取任务队列之前触发回调函数 也就是说当前执行栈的最后面执行,下一次执行栈之前执行 另外不要写下面的死递归代码

// 当前的执行栈会永远执行不完,而任务队列永远读不了
process.nextTick(function foo() {
  process.nextTick(foo);
});
  • setImmediate 当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行 和setTimeout(() => {}, 0)很相似

  • setTimeout(() => {}, time)

过了time时间点以后, 下一次主线程读取任务队列时触发回调函数

  • requestAnimationFrame

同setTimeout, 但是更流畅,一般用这个api实现函数节流

eventLoop 解释: 将变量和函数的声明放入全局内存(堆)中, 将函数的调用放入调用堆栈.

canvas的用法

canvas怎么用具体还是看canvas的mdn文档. api比较多,功能很强大。比较流行写h5游戏, 处理视频, 图表生成。

  • 画布生成图像
const canvas = document.getQuerySelector('canvas')
if (canvas.getContext) {
  let imgUrl = canvas.toDataURL('image/png')
  let image = document.createElement('img')
  image.src = imgUrl
  document.body.appendChild(image)
}
  • 图像变成灰度图
const filterColor = function(context){

  var img = document.images[0]
  var imageData, data, i, len, average, red, green, blue, alpha

  context.drawImage(img, 0, 0)
  imageData = context.getImageData(0, 0, img.width, img.height)
  data = imageData.data

  for(i = 0, len = data.length; i < len; i+=4){
    red = data[i]
    green = data[i+1]
    blue = data[i+2]
    alpha = data[i+3]

    average = Math.floor((red + green + blue) / 3)
    data[i] = data[i+1] = data[i+2] = average
  }

  imageData.data = data
  context.putImageData(imageData, 0, 0)
}

继承方式与原型链

首先讲一下javascript中原型链的一些定义:

  • 每个对象都有 __proto__ 属性,用于指向创建它的构造函数的原型对象
  • 每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象 在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数) 属性
  • 实例由构造函数初始化,可以继承创建它的构造函数的原型对象的方法
const log = console.log.bind(console)
// Person构造函数 [Function: Person]
function Person(name) {
  this.name = name
}
Person.prototype.sayName = function () {
    console.log('my name:', this.name)
}


let woyao = new Person('woyao')
// 实例由构造函数初始化,可以继承创建它的构造函数的原型对象的方法
woyao.sayName()

// 获得woyao这个实例的构造函数
log(woyao.constructor) // [Function: Person]
// 每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象
// 每个对象都有 `__proto__` 属性,用于指向创建它的构造函数的原型对象
log(woyao.__proto__ === Person.prototype)

了解了上面的定义之后讲讲在new一个构造函数生成一个实例的时候做了什么操作:

  • 创建一个对象,继承自一个原型对象,就是指定实例对象的__proto__为构造函数的原型对象
  • 构造函数执行,this指定为这个实例对象
  • 如果构造函数返回了一个对象,则该对象取代步骤一: 创建的对象
  • 如果没有,就不替换
// func是构造函数
const New = (func, ...args) => {
    let obj = Object.create(func.prototype)
    let result = func.call(obj, ...args)
    if (typeof result === 'object') {
        return result
    }
    return obj
}

let woyao = New(Person, 'woyao')
log('woyao', woyao)
woyao.sayName()

继承方式

  • 构造函数继承 缺点: 父类的原型对象的方法, 子类拿不到
function Parent1() {
    this.name = 'xxx'
}

function Child1() {
    // 执行父类的构造函数
    Parent1.call(this)
    this.type = 'child1'
}
  • 原型链对象继承 缺点: 实例对象因为都是继承了父类的实例,通过对父类实例的修改 会对各个实例之间都产生影响
const log = console.log.bind(console)

function Parent2() {
    this.name = 'xxx'
    this.arr = [1, 2]
}

function Child2() {
    this.type = 'child1'
}

// new Child2().__proto__ === Child2.prototype === new Parent2()
// 实例能够继承构造函数的原型对象上的属性和方法
Child2.prototype = new Parent2()

let child1 = new Child2()
let child2 = new Child2()
child1.arr.push(3)
log(child2.arr)
  • 组合继承
// 第一版:
// 缺点:父类构造函数执行了2次,第二次构造函数的执行取代了第一次
function Parent3() {
    this.name = 'xxxx'
}
function Child3() {
    Parent3.call(this)
    this.type = 'child3'
}
Child3.prototype = new Parent3()

// 第二版:
// 缺点: 实例的constructor指向的是父类
function Parent4() {
    this.name = 'xxxx'
}
function Child4() {
    Parent4.call(this)
    this.type = 'child4'
}

Child4.prototype = Parent4.prototype
let c4_1 = new Child4()
log(c4_1.constructor) // Parent4

// 最终版
function Parent5() {
    this.name = 'xxxx'
}
function Child5() {
    Parent5.call(this)
    this.type = 'child5'
}
Child5.prototype = Object.create(Parent5.prototype)
Child5.prototype.constructor = Child5

DOM常见操作

节点查找

const e = (element, attribute) => element.querySelector(attribute)
const es = (element, attribute) => element.querySelectorAll(attribute)

节点创建

// 创建文本节点
document.createTextNode('text')
// 创建元素
document.createElement('div')
// 克隆节点
e('.test').cloneNode(true)

节点修改(添加,删除,替换)

parent.appendChild(child)
parentNode.insertBefore(newNode, refNode)
// insertAdjacentHTML的第一个参数用法如下:
/*
<!-- beforebegin 作为最近的相邻兄弟节点-->
<p class="block">
  <!-- afterbegin -->
  foo
  <!-- beforeend 作为直接子元素中的最后一个-->
</p>
<!-- afterend -->
*/
el.insertAdjacentHTML('beforeend', htmlString)
targetElement.insertAdjacentElement(position, element)
let deletedChild = parent.removeChild(node);
parent.replaceChild(newChild, oldChild)

节点属性

element.setAttribute(name, value)
let value = element.getAttribute("id")
let result = element.hasAttribute(name)
e('#user').dataset.user

节点关系

parentNode :每个节点都有一个parentNode属性,它表示元素的父节点。Element的父节点可能是Element,Document或DocumentFragment;

parentElement :返回元素的父元素节点,与parentNode的区别在于,其父节点必须是一个Element元素,如果不是,则返回null;

children :返回一个实时的 HTMLCollection ,子节点都是Element,IE9以下浏览器不支持;

childNodes :返回一个实时的 NodeList ,表示元素的子节点列表,注意子节点可能包含文本节点、注释节点等;

firstChild :返回第一个子节点,不存在返回null,与之相对应的还有一个 firstElementChild ;

lastChild :返回最后一个子节点,不存在返回null,与之相对应的还有一个 lastElementChild ;

previousSibling :节点的前一个节点,如果不存在则返回null。注意有可能拿到的节点是文本节点或注释节点,与预期的不符,要进行处理一下。

nextSibling :节点的后一个节点,如果不存在则返回null。注意有可能拿到的节点是文本节点,与预期的不符,要进行处理一下。

previousElementSibling :返回前一个元素节点,前一个节点必须是Element,注意IE9以下浏览器不支持。

nextElementSibling :返回后一个元素节点,后一个节点必须是Element,注意IE9以下浏览器不支持。

样式操作

elem.style.color = 'red';
elem.style.setProperty('font-size', '16px');
elem.style.removeProperty('color');

div.classList.remove("foo");
div.classList.add("anotherclass");
div.classList.toggle("visible");
div.classList.replace("foo", "bar");

let style = window.getComputedStyle(element[, pseudoElt]);

jquery常用api

  • 选择器

    1. 普通选择器

    2. find

    3. siblings

    4. closest, parent

  • dom 操作

    1. append

    2. remove

    3. empty

    4. show, hide, toggle

  • class 操作

    1. addClass removeClass

    2. toggleClass

  • 属性、特性操作

    1. attr, prop, data , prop 用于 true false 这样的布尔值属性

    2. removeAttr

  • 取值

    1. val

    2. text

    3. html

reflect, proxy, Symbol的用法

  • ==Proxy== 为其他对象提供一种代理以控制对这个对象的询问 大白话就是你要做的事情我给你来做,如果事情我能做的话, 这样事情就可以按照我想要的意愿发生

    // 其实也就是个代理模式罢了, es6创建了一个Proxy类
    // 对代理模式不懂可以看看我的设计模式文章(https://juejin.cn/post/6960665002449731598)
    // 给target对象提供一个代理,通过handler对target做操作
    // 大白话就是你要做的事情我给你来做,如果事情我能做的话, 这样事情就可以按照我想要的意愿发生
    // Proxy(target, handler)
    /*
    es6 实现的Proxy代理类中定义了下面常用的劫持方法:
    1. apply(target, thisArgument, ...args):   在代理对象调用的时候劫持
    2. construct(target, ...args): new 操作的时候进行劫持
    3. defineProperty(target, prop, attributes): Object.defineProperty的时候劫持
    4. get(target, prop, receiver): 获取属性值的时候劫持
    5. has(target, prop): in 操作的时候劫持
    6. set(target, prop, value, receiver): 设置属性的时候劫持
    7. ownKeys(target): Object.getOwnPropertyNames和Object.geetOwnPropertySymbols调用劫持
    */
    const target = {
      type: "human",
    }
    
    const handler = {
      // 我想要获取属性的时候结果是如下的
      get: function(target, prop, receiver) {
        if (prop === "type") {
          return "pig";
        }
        // return Reflect.get(target, prop, receiver)
        return Reflect.get(...arguments);
      },
      // 你说他的名字是xxx, 我说他是个xxx
      set: function(target, prop, value) {
        if (prop === "name") {
          target.name = "万恶的资产阶级";
        }
      }
    }
    
    const proxy = new Proxy(target, handler)
    // 事情按照我想要的情况发生了
    console.log(proxy.type) // pig
    proxy.name = '某公司'
    log(proxy.name)
    
  • ==reflect== 是一个内置的对象,它提供拦截 JavaScript 操作的方法 虽然和Proxy一样都是代理拦截。但是reflect不改变事情发生的结果 大白话就是你要做的事情我给你来做,如果事情我能做的话,我就帮你做了

    // 其实reflect这个对象主要是用来配合Proxy这个构造对象来用的
    // 在Proxy的handler中所有的劫持方法, reflect都有
    // 一件事情本来是什么样的我可以通过reflect调用来知道
    // 大白话就是你要做的事情我给你来做,如果事情我能做的话,我就帮你做了
    const handler = {
      // 我想要获取属性的时候结果是如下的
      get: function(target, prop, receiver) {
        if (prop === "type") {
          return "pig";
        }
        // 如果prop不是type的话,属性值原本是啥样就是啥样
        // return Reflect.get(target, prop, receiver)
        return Reflect.get(...arguments);
      },
    }
    
    const duck = {
      name: 'Maurice',
      color: 'white',
      greeting: function() {
        console.log(`Quaaaack! My name is ${this.name}`);
      }
    }
    
    // 'color' in duck
    Reflect.has(duck, 'color')
    // Object.getOwnPropertyNames(duck)
    Reflect.ownKeys(duck)
    
    
  • ==Symbol== 生成一个全局唯一的值

    var sym1 = Symbol('foo');
    var sym2 = Symbol('foo');
    console.log(sym1 === sym2) // false
    
    const obj = {
        [Symbol('a')]:'Hello',
        [Symbol('b')]:'world'
    }
    
    // Object.getOwnPropertyNames 不能获取key为Symbol类型的属性
    // [Symbol(a), Symbol(b)]
    const a = Object.getOwnPropertySymbols(obj)
    
    let obj = {
      [Symbol('my_key')]: 1,
      enum: 2,
      nonEnum: 3
    }
    // for...of, for...in 中也不能获取到Symbol属性
    // ["enum", "nonEnum", Symbol(my_key)]
    Reflect.ownKeys(obj)
    
    // Symbol.for
    // 使用给定的key搜索现有的symbol,
    // 如果找到则返回该symbol。否则将使用给定的key在全局symbol注册表中创建一个新的symbol
    // Symbol.for()会被登记在全局环境中
    let s1 = Symbol.for('foo')
    let s2 = Symbol.for('foo')
    
    console.log(s1 === s2) // true
    
    // Symbol.keyFOr
    // 从全局symbol注册表中,为给定的symbol检索一个共享的symbol key
    // Symbol.for 是的s1登记在全局注册表中
    let s1 = Symbol.for("foo")
    console.log(Symbol.keyFor(s1)) // "foo"
    
    let s2 = Symbol("foo")
    Symbol.keyFor(s2) // undefined
    

函数防抖与节流

防抖通常被叫做debounce, 节流叫做throttle. 它们的作用是用来控制事件对应的异步任务的执行频率。 简单来讲就是不能让一些异步任务总是在每一轮EventLoop中的主线程中老是执行。因为这样就抢占了 其他任务被执行的快慢。而本身自己也不需要执行那么多次。

debounce

这个函数的大白话就是:多次执行只有一次有效,讲白了就是希望当前事件连续发生时对应的所有执行任务的最后一个任务生效,另外有些情况也会希望事件第一次发生的时候也运行一次

  • 常被用到的场景
    • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
    • 手机号、邮箱验证输入检测
    • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
const log = console.log.bind(console)
// 场景:
// 鼠标在页面上移动的时候,弹出恶意广告
let later
const debounce = (fn, timer, ...args) => {
    clearTimeout(later)
    later = setTimeout(fn, timer, ...args)
}

const alertAds = (event) => {
    log(`point at (${event.x}, ${event.y}) ads: 双十一就是淦`)
}

document.addEventListener('mousemove', (event) => {
    debounce(alertAds, 1000, event)
})
// 上面的debounce的写法不太好
// 因为later暴露在外面了,这样如果把debounce作为一个库函数导出的时候就没有封装的美感
// 所以应该用闭包的机制把later包起来,作为局部的全局变量
const log = console.log.bind(console)

const debounce = (fn, timer) => {
    let later;
    return (...args) => {
        later && clearTimeout(later)
        later = setTimeout(fn, timer, ...args)
    }
}

const alertAds = (event) => {
    log(`point at (${event.x}, ${event.y}) ads: 双十一就是淦`)
}

const debounceAlertAds = debounce(alertAds, 1000)

document.addEventListener('mousemove', (event) => {
	  // 这里必须把它变成实名函数
		// debounce(alertAds, 1000)(event)的写法是不会生效的
		// 因为事件的回调函数是一个匿名函数
    // 每次事件的发生,都会重新创建一个新的函数作用域
    // 那么debounce运行的时候,都会生成一个新的later
  	// 写成具体的debounceAlertAds以后, later就等于是个全局的了
    debounceAlertAds(event)
})
// 上面写的debounce都是当前事件连续发生时所对应的执行任务的最后一个任务生效
// 但是有的场景需要事件第一次发生的时候也运行一次
// 场景:
// 鼠标在页面上连续移动的时候,弹出恶意广告
// 哈哈,真心想不到啥场景,就瞎弄了个,凑合下吧, 23333
const log = console.log.bind(console)

const alertAds = (event) => {
    log(`point at (${event.x}, ${event.y}) ads: 双十一就是淦`)
}

const debounce = (func, timer, immediate) => {
    let later
    return (...args) => {
        if (!later && immediate) {
            func(...args)
        }
        later && clearTimeout(later)
        later = setTimeout(func, timer, ...args)
    }
}

const debounceAlertAds = debounce(alertAds, 1000, true)

document.addEventListener('mousemove', (event) => {
    debounceAlertAds(event)
})

throttle

这个函数的大白话就是:技能有cd,只有冷却时间到了以后才能发出技能.讲白了就是希望当前事件连续发生时对应的所有执行任务中满足到了冷却时间点的任务被执行

  • 常被用到的场景
    • 滚动加载,加载更多或滚到底部监听
    • 谷歌搜索框,搜索联想功能
    • 高频点击提交,表单重复提交
// 好吧,场景还是鼠标在页面上连续移动的时候,弹出恶意广告。

const log = console.log.bind(console)

const alertAds = (event) => {
    log(`point at (${event.x}, ${event.y}) ads: 双十一就是淦`)
}

const throttle = (fn, timer) => {
    let later
    return (...args) => {
        if (later) return
        later = setTimeout(() => {
            fn(...args)
            later = null
        }, timer)
    }
}

const throttleAlertAds = throttle(alertAds, 1000)
document.addEventListener('mousemove', (event) => {
    throttleAlertAds(event)
})

js内存管理

内存生命周期

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

内存分配

这里我是抄的这个人的关于垃圾回收机制的文章mdn的js内存管理 写的都挺好的。

  • JavaScript 在定义变量时就完成了内存分配
// 在js中一切都是对象,对象声明赋值(定义)以后就会自动分配内存
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
function f(a){
  return a + 2;
} // 给函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);
  • 使用值的过程实际上是对分配内存进行读取与写入的操作

  • 内存释放-垃圾回收机制(跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它 大白话就是:监视所有对象,并删除那些不可访问的对象 js的垃圾回收机制算法如下:

    • 引用计数

      // 引用计数垃圾收集
      
      // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
      // 很显然,没有一个可以被垃圾收集
      var o = {
      a: {
          b:2
      }
      };
      
      var o2 = o; // o2变量是第二个对“这个对象”的引用
      o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
      
      // 引用“这个对象”的a属性
      // 现在,“这个对象”有两个引用了,一个是o2,一个是oa
      var oa = o2.a;
      
      // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
      // 但是它的属性a的对象还在被oa引用,所以还不能回收
      o2 = "yo"
      
      // a属性的那个对象现在也是零引用了
      // 它可以被垃圾回收了
      oa = null
      
    • 循环引用 该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收(通过标记清除算法解决)。

      /* 注意在js的root中找不到o,o2的定义,会被标记清除 */
      function f(){
        var o = {};
        var o2 = {};
        o.a = o2; // o 引用 o2
        o2.a = o; // o2 引用 o
      
        return "azerty";
      }
      
      f();
      
    • 标记清除 设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

      • 垃圾回收器获取根并“标记”(记住)它们。

      • 然后它访问并“标记”所有来自它们的引用。

      • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。

      • 以此类推,直到有未访问的引用(可以从根访问)为止。

      • 除标记的对象外,所有对象都被删除。 解决了上述的循环引用因为引用计数不会被回收的问题.

    • 具体实例:

      1. 编写一段代码如下
      function marry (man, woman) {
        woman.husban = man;
        man.wife = woman;
      
        return {
          father: man,
          mother: woman
        }
      }
      
      let family = marry({
        name: "John"
      }, {
        name: "Ann"
      })
      
      1. 在引用标记内存结构如下

      内存管理1.png

      1. 删除以下引用
        delete family.father;
        delete family.mother.husband;
      
      1. 删除后的内存结构如下

        内存分配2

        内存分配3

      2. 然后删除famlily的引用,根据标记清除的原则后内存结构如下

        family = null;
        

        最终结果