JavaScript技巧代码片段
使用navigator对象获取媒体设备相关数据
navigator.mediaDevices(); // 返回可用的媒体设备
navigator.getUserMedia(); // 返回可用媒体设备硬件关联的流
使用typeof判断一个变量是否存在
typeof操作符返回一个字符串,表示未经计算的操作数的类型:
if(typeof a != 'undefined'){
// 变量存在
}
不能使用if(a),若a未声明,则报错
instanceof的实现原理
function myInstanceof(left,right){
// 这里先用typeof判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) {
return false
}
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getProtypeOf(left);
while(true) {
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
顺着原型链去找,直到找到相同的原型对象,返回true,否则为false
typeof 与 instanceof 区别
typeof instanceof都是判断数据类型的方法,区别如下:
- typeof 会返回一个变量的基本类型,instanceof返回的是一个布尔值
- instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
- typeof也存在弊端,它虽然可以判断基础数据类型(null除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断。
如果需要通过检测数据类型,可以采用
Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串。 下面为一个全局通用的数据类型判断方法。
function getType(obj){
let type = typeof obj;
// 先进行typeof判断,如果是基础数据类型,直接返回
if(type !== "object"){
return type;
}
//对于typeof返回结果是object,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,'$1')
}
手写new操作符
new关键字工作流程:
- 创建一个新的对象obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的this绑定到新建的对象obj上
- 根据构建函数返回类型作判断,如果是原始值则被忽略,如果返回对象,需要正常处理
function mynew(Func,...args){
// 1.创建一个新对象
const obj = {};
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj,args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
bind,call,apply 手写bind
实现bind的步骤,分解成三步
- 修改this指向
- 动态传递参数
//方式1:只在bind中传递函数参数
fn。bind(obj,1,2)()
//方式2:在bind中传递函数参数,也返回函数中传递参数
fn.bind(obj,1)(2)
- 兼容new关键字 代码如下:
Function.prototype.myBind = function(context){
// 判断调用对象是否为函数
if(typeof this !== "function"){
throw new TypeError("Error")
}
//获取参数
const args = [...arguments].slice(1),
fn = this;
return function Fn(){
//根据调用方式,传入不同绑定值
return fn.apply(this instanceof Fn ? new fn(...arguments) : context,args.concat(...arguments));
}
}
闭包柯里化函数
闭包
闭包是一个函数和对周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。 闭包让你可以在一个内层函数中访问到其外层函数的作用域。 闭包使用场景:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的。
柯里化函数
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。
//假设我们又一个求长方形面积的函数
function getArea(width,height){
return width*height
}
//如果我们碰到长方形的宽老是10
const area1 = getArea(10,20)
const area2 = getArea(10,30)
const area3 = getArea(10,40)
//我们可以使用闭包柯里化这个计算面积的函数
function getArea(width){
return height => {
return width*height
}
}
const getTenWidthArea = getArea(10)
//之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
//而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
使用闭包模拟私有方法
在JavaScript中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法
var Counter = (function(){
var privateCounter = 0;
function changeBy(val){
privateCounter +=val;
}
return {
increment:function(){
changeBy(1);
},
decrement:function(){
changeBy(-1);
},
value:function(){
return privateCounter;
}
}
})();
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上述通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式。 两个计数器Counter1和Counter2是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量。
其他注意
例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期。 如果某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响 例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因在于每个对象的创建,方法都会被重新赋值。
function MyObject(name,message){
this.name = name.toString();
this.message = message.toString();
this.getName = function(){
return this.name;
}
this.getMessage = function(){
return this.message;
}
}
上面的代码中,并没有利用到闭包的好处,因此可以避免使用闭包。修改如下:
function MyObject(name,message){
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function(){
return this.name;
}
MyObject.prototype.getMessage = function(){
return this.message;
}
手写代码实现浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。 即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址 下面实现简单浅拷贝。
function shallowClone(obj){
const newObj = {};
for(let prop in obj){
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
手写代码实现深拷贝
深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。 常见的深拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.string()
- 手写循环递归 WeakMap:WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。
function deepClone(obj,hash = new WeakMap){
if(obj === null)return obj;//如果是null或者undefined 则不进行拷贝操作
if(obj instanceof Date) return new Date(obj);
if(obj instanceof RegExp) return new RegExp(obj);
//可能是对象或者普通的值,如果是函数的话是不需要深拷贝。
if(typeof obj!== "object") return obj;
//是对象的话才进行深拷贝
if(hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
//找到的是所属类原型上的constructor,而原型上的consturctor指向的是当前类本身
hash.set(obj,cloneObj);
for(let key in obj){
if(obj.hasOwnProperty(key)){
//实现一个递归拷贝
cloneObj[key] = deepClone(obj[key],hash);
}
}
return cloneObj;
}
手写节流与防抖
节流
定义:n秒内只运行一次,若在n秒内重复触发,只有一次生效。
function throttled1(fn,delay = 500){
let oldtime = Date.now()
return function(...args){
let newtime = Date.now()
if(newtime - oldtime >= delay){
fn.apply(null,args)
oldtime = Date.now()
}
}
}
使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后,依然会再一次执行。
funtion throttled2(fn,delay = 500){
let timer = null
return function (...args){
if(!timer){
timer = setTimeout(()=>{
fn.apply(this,args)
timer = null
},delay);
}
}
}
可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下:
function throttled(fn,delay){
let timer = null
let starttimer = Date.now()
return function () {
let curtime = Date.now() //当前时间
let remaining = delay - (curTime - starttime) //从上一次到现在,还剩下多少多余时间
let context = this
let args = argumnents
clearTimeout(timer)
if(remaining <= 0){
fn.apply(context,args)
starttime = Date.now()
}else{
timer = setTimeout(fn,remaining);
}
}
}