js相关
一、Object.defineProperty()方法
vue的双向数据绑定就是通过Object.defineProperty()方法实现的,俗称属性拦截器。
方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
// 语法: /* * @param: obj:需要定义属性的对象; * prop:需要定义或修改的属性; * descriptor:将被定义或修改属性的描述符 */ Object.defineProperty(obj,prop,descriptor)
实例:
var a = {}; Object.defineProperty(a, 'b', { set: function(newValue) { console.log('赋值操作, 赋值' + newValue); }, get: function() { console.log('取值操作'); return 2; } }); a.b = 1; // 赋值操作,赋值1 a.b; // 取值操作2
下面我们来写个实例看看,这里我们以Vue为参照去实现怎么写MVVM
// index.html <body> <div id="app"> <h1>{{song}}</h1> <p>《{{album.name}}》是{{singer}}2005年11月发行的专辑</p> <p>主打歌为{{album.theme}}</p> <p>作词人为{{singer}}等人。</p> 为你弹奏肖邦的{{album.theme}} </div> <!--实现的mvvm--> <script src="mvvm.js"></script> <script> // 写法和Vue一样 let mvvm = new Mvvm({ el: '#app', data: { // Object.defineProperty(obj, 'song', '发如雪'); song: '发如雪', album: { name: '十一月的萧邦', theme: '夜曲' }, singer: '周杰伦' } }); </script> </body>
上面是html里的写法,相信用过Vue的同学并不陌生
那么现在就开始实现一个自己的MVVM吧
打造MVVM
// 创建一个Mvvm构造函数
// 这里用es6方法将options赋一个初始值,防止没传,等同于options || {}
function Mvvm(options = {}) {
// vm.$options Vue上是将所有属性挂载到上面
// 所以我们也同样实现,将所有属性挂载到了$options
this.$options = options;
// this._data 这里也和Vue一样
let data = this._data = this.$options.data;
// 数据劫持
observe(data);
}
数据劫持
为什么要做数据劫持?
-
观察对象,给对象增加Object.defineProperty
-
vue特点是不能新增不存在的属性 不存在的属性没有get和set
-
深度响应 因为每次赋予一个新对象时会给这个新对象增加defineProperty(数据劫持)
多说无益,一起看代码
// 创建一个Observe构造函数 // 写数据劫持的主要逻辑 function Observe(data) { // 所谓数据劫持就是给对象增加get,set // 先遍历一遍对象再说 for (let key in data) { // 把data属性通过defineProperty的方式定义属性 let val = data[key]; observe(val); // 递归继续向下找,实现深度的数据劫持 Object.defineProperty(data, key, { configurable: true, get() { return val; }, set(newVal) { // 更改值的时候 if (val === newVal) { // 设置的值和以前值一样就不理它 return; } val = newVal; // 如果以后再获取值(get)的时候,将刚才设置的值再返回去 observe(newVal); // 当设置为新值后,也需要把新值再去定义成属性 } }); } } // 外面再写一个函数 // 不用每次调用都写个new // 也方便递归调用 function observe(data) { // 如果不是对象的话就直接return掉 // 防止递归溢出 if (!data || typeof data !== 'object') return; return new Observe(data); }
以上代码就实现了数据劫持
二、巧用JSON.stringify()生成漂亮格式的JSON字符串
就是通过 JSON.stringify()
函数的第三个参数来指定缩进的空格数:
// 此处为了示例, 采用字面量的形式构造了一个对象
// 实际使用中, 一般是某个POJO,或者VO之类的值对象
var myObject = {
"myProp": "myValue",
"subObj": {
"prop": "value"
}
};
// 格式化
var formattedStr = JSON.stringify(myObject, null, 2);
生成的字符串如下所示:
{ "myProp": "myValue", "subObj": { "prop": "value" } }
三、实现一个 sleep 函数
比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现。
1.
//Promise const sleepp = time => { return new Promise(resolve => setTimeout(resolve,time)) } sleepp(1000).then(()=>{ console.log(1) })
2.
//Generator function* sleepGenerator(time) { yield new Promise(function(resolve,reject){ setTimeout(resolve,time); }) } sleepGenerator(1000).next().value.then(()=>{console.log(1)})
3.
//async function sleep(time) { return new Promise(resolve => setTimeout(resolve,time)) } async function output() { let out = await sleep(1000); console.log(1); return out; } output();
4.
//ES5 function sleep(callback,time) { if(typeof callback === 'function') setTimeout(callback,time) } function output(){ console.log(1); } sleep(output,1000);
四、数组去重并获取重复元素次数
const AA = [1,2,3,4,2,1,2,2,4,6,4] function filter (arr) { var newArr = [] var obj={} for(var i = 0; i < arr.length; i++){ if(obj[arr[i]]){ obj[arr[i]]++ }else{ newArr.push(arr[i]) obj[arr[i]]=1 } } var res = [] for (var n in obj) { if(obj[n] > 1) { res.push(`重复元素:${n},重复了${obj[n]}次`) } } return res } console.log(filter(AA))
五、合并数组并去重
// 数组合并并去重 let arr1 = [1, 1, 2, 3, 't', 9, 5, 5, 4] let arr2 = [1, 2, 5, 4, 9, 7, 7, 't', 8]
function uniqueArr(arr1,arr2) { //合并两个数组 arr1.push(...arr2)//或者arr1 = [...arr1,...arr2] //去重 let arr3 = Array.from(new Set(arr1))//let arr3 = [...new Set(arr1)] return arr3 } console.log(uniqueArr(arr1, arr2))
六、js实现汉字中文排序的方法 例如:省市列表的排序
1.数组内的元素是中文字符串的简单排序
var arr = ['南京', '北京', '上海', '杭州', '深圳']; function sortChinese (arr) { // 参数: 排序的数组 arr.sort(function (item1, item2) { return item1.localeCompare(item2, 'zh-CN'); }) } sortChinese(arr) console.log(arr); // ["北京", "杭州", "南京", "上海", "深圳"]
2.数组内的元素是对象,以对象某一个属性进行排序
var arr = [ {name: '南京', code: '09', info: {province: '江苏'}}, {name: '北京', code: '01', info: {province: '北京'}}, {name: '上海', code: '02', info: {province: '上海'}}, {name: '深圳', code: '05', info: {province: '广东'}} ]; function sortChinese (arr, dataLeven) { // 参数:arr 排序的数组; dataLeven 数组内的需要比较的元素属性 /* 获取数组元素内需要比较的值 */ function getValue (option) { // 参数: option 数组元素 if (!dataLeven) return option var data = option dataLeven.split('.').filter(function (item) { data = data[item] }) return data + '' } return arr.sort(function (item1, item2) { return getValue(item1).localeCompare(getValue(item2), 'zh-CN'); }) } console.log(sortChinese(arr, 'name') // 例如:比较的是name,传入的就是 'name'
); /*[{name: '北京', code: '01', info: {province: '北京'}}, {name: '南京', code: '09', info: {province: '江苏'}}, {name: '上海', code: '02', info: {province: '上海'}}, {name: '深圳', code: '05', info: {province: '广东'}}]*/ sortChinese(arr, 'info.province') // 例如:比较的是数组元素属性info内的province属性,传入的就是 'info.province' console.log(arr); /* [{name: '北京', code: '01', info: {province: '北京'}}, {name: '深圳', code: '05', info: {province: '广东'}}, {name: '南京', code: '09', info: {province: '江苏'}}, {name: '上海', code: '02', info: {province: '上海'}}]*/
3.对国内的所有省份进行排序,并且首字母相同的第一个添加首字母
function chineseLetter (arr, dataLeven) { var letter = 'abcdefghjklmnopqrstwxyz'.split('') var zh = "阿八嚓哒妸发旮哈讥咔垃痳拏噢妑七呥扨它穵夕丫帀".split('') /* 获取数组元素比较的值 */ function getValue (option) { if (!dataLeven) return option var data = option dataLeven.split('.').filter(function (item) { data = data[item] }) return data + '' } /* 进行排序 */ arr.sort(function (item1, item2) { return getValue(item1).localeCompare(getValue(item2), 'zh-Hans-CN') }) /* 判断需要排序的字符串是否含有中文字符 */ if (/[\u4e00-\u9fff]/.test(getValue(arr[0])) && typeof arr[0] === 'object') pySegSort(0, 0) /* 给省列表中添加首字符 */ function pySegSort (letterIndex, zhIndex) { var first = true // 首次是否加 字母标识 for (var i = zhIndex; i < arr.length; i++) { var item = arr[i] // 是否有值 && 当前值大于等于本次字母的最小值 && (最后一位 || 当前值小于下次字母的最小值) var state = zh[letterIndex] && getValue(item).localeCompare(zh[letterIndex], 'zh') >= 0 && (letterIndex === letter.length - 1 || getValue(item).localeCompare(zh[letterIndex+1], 'zh') < 0) if (state) { // 满足条件,同一个首字母下的:例如 A 下的所有省份 if (first) { //是否是第一次出现 item.letter = letter[letterIndex].toUpperCase() first = false } else { item.letter = '' } } else { // 递归调用 函数,进行下次字母下的排列 letterIndex++ if (letterIndex < letter.length) { pySegSort(letterIndex, i) break } } } } } chineseLetter(provinceList, 'value') console.log(provinceList)
七、js事件循环、宏任务和微任务
事件循环中的同步任务,异步任务:
-
同步和异步任务在不同的执行"场所",同步的进入主线程,异步的进入Event Table执行并注册函数。
-
当指定的异步事情完成时,Event Table会将这个函数移入Event Queue。
-
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,推入主线程执行。
-
js引擎的monitoring process进程会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。上述过程会不断重复,也就是常说的Event Loop(事件循环)。
console.log('先执行这里'); setTimeout(() => { console.log('执行啦') },3000);//先执行这里// ... 3s later 3秒到了,计时事件timeout完成,定时器回调进入Event Queue,主线程执行已执行完,此时执行定时器回调。 // 执行啦
setTimeout(() => { task() },3000) sleep(10000000)
上述代码在控制台执行task()需要的时间远远超过3秒,执行过程:
-
task()进入Event Table并注册,计时开始。
-
执行sleep函数,很慢,非常慢,计时仍在继续。
-
3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
-
sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
上述的流程走完,setTimeout这个函数是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
重点来了:定时器的毫秒,不是指过ms秒执行一次fn,而是过ms秒,会有fn进入Event Queue。
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。
对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
宏任务和微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
-
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
-
micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
在宏任务和微任务概念中的事件循环机制:
主任务(宏任务)完——所有微任务——宏任务(找到宏任务其中一个任务队列执行,其中如果又有微任务,该任务队列执行完就执行微任务)——宏任务中另外一个任务队列(里面偶微任务就再执行微任务)。
总的来说就是在宏任务和微任务之间来回切。下面列子执行过程:
第一轮:主线程输出:【1,7】,添加宏任务【set1,set2】,添加微任务【6,8】。执行完主线程,然后执行微任务输出【6,8】
第二轮:执行宏任务其中一个任务队列set1:输出【2,4】,执行任务的过程,碰到有微任务,所以在微任务队列添加输出【3,5】的微任务,在set1宏任务执行完就执行该微任务,第二轮总输出:【2,4,3,5】
第三轮:执行任务另一个任务队列set2:输出【9,11】,执行任务的过程,碰到有微任任务,所以在微任务队列添加输出【10,12】的微任务,在set2宏任务执行完就执行该微任务,第三轮总输出:【9,11,10,12】
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
console.log('1'); //第一轮主线程【1】 setTimeout(function() { //碰到set异步,丢入宏任务队列【set1】:我将它命名为set1 console.log('2');//第二轮宏任务执行,输出【2】 process.nextTick(function() {//第二轮宏任务执行,碰到process,丢入微任务队列,【3】 console.log('3'); }) new Promise(function(resolve) {//第二轮宏任务执行,输出【2,4】 console.log('4'); resolve(); }).then(function() { console.log('5')//第二轮宏任务执行,碰到then丢入微任务队列,【3,5】 }) }) process.nextTick(function() { //碰到process,丢入微任务队列【6】 console.log('6'); //第一轮微任务执行 }) new Promise(function(resolve) { console.log('7'); //new的同时执行代码,第一轮主线程此时输出【1,7】 resolve(); }).then(function() { console.log('8') //第一轮主线程中promise的then丢入微任务队列,此时微任务队列为【6,8】。当第一轮微任务执行,顺序输出【6,8】 }) setTimeout(function() { //碰到set异步丢入宏任务队列,此时宏任务队列【set1.set2】:我将它命名为set2 console.log('9');//第三轮宏任务执行,输出【9】 process.nextTick(function() { //第三轮宏中执行过程中添加到微任务【10】 console.log('10'); }) new Promise(function(resolve) { console.log('11');//第三轮宏任务执行,宏任务累计输出【9,11】 resolve(); }).then(function() { console.log('12') //第三轮宏中执行过程中添加到微任务【10,12】 }) })
八、防抖与节流
防抖指触发事件后在n秒内函数只执行一次,若在n秒内再次触发则重新计算
节流指连续发生的事件在n秒内只执行一次函数
防抖和节流通常用于优化即时查询
九、正则
1.匹配url地址
let strUrl = 'https://www.bilibili.com/video/BV1n4411m7TQ/?p=7'
let regUrl = /^(?:(http|https|ftp):\/\/)?((?:[\w-]+\.)+[a-z0-9]+)((?:\/[^/?#]*)+)?(\?[^#]+)?(#.+)?$/i;
console.log(regUrl.exec(strUrl))
十、数据类型检测
在JS中有这几种方法来判断数据类型:typeof、instanceof、Object.prototype.toString.call()、constructor
1.typeof
Array,Object,null,Date,RegExp,Error这几个类型都被 typeof 判断为 object,所以如果想要判断这几种类型,就不能使用 typeof 了。
Number,String,Boolean,Function,undefined,如果想判断这几种类型,那就可以使用 typeof。
var o = new Array(); console.log(typeof 1);//number console.log(typeof 'xiaoming');//string console.log(typeof true);//boolean console.log(typeof undefined);//undefined console.log(typeof null);//object console.log(typeof o);//object console.log(typeof new RegExp());//object
2. instanceof
object instanceof constructor -> object表示某个实例对象,contructor表示某个构造函数,用来检测 constructor.prototype 是否存在于参数 object 的原型链上(判断对象是不是另一个对象实例),如:
function Foo(){} var foo = new Foo() console.log(foo instanceof Foo); // true
console.log([] instanceof Array);//true console.log({} instanceof Object);//true console.log(function(){} instanceof Function);//true
instanceof也可以判断简单数据类型,但是简单数据类型不能以字面量的形式创建,否则返回值为false;如果通过new关键字去创建简单数据类型,则返回值为true; instanceof不能区别undefined和null。
console.log(1 instanceof Number);//false console.log('xiaoming' instanceof String);//false console.log(true instanceof Boolean);//false console.log('--------------------------------'); console.log(new Number(1) instanceof Number);//true console.log(new String('xiaoming') instanceof String);//true console.log(new Boolean(true) instanceof Boolean);//true
console.log( 123 instanceof Number, //false 'dsfsf' instanceof String, //false false instanceof Boolean, //false [1,2,3] instanceof Array, //true {a:1,b:2,c:3} instanceof Object, //true function(){console.log('aaa');} instanceof Function, //true undefined instanceof Object, //false null instanceof Object, //false new Date() instanceof Date, //true /^[a-zA-Z]{5,20}$/ instanceof RegExp, //true new Error() instanceof Error //true )
3. Object.prototype.toString.call()
Object.prototype.toString.call()能够准确的判断所有的数据类型;
alert(Object.prototype.toString.call('') === ‘[object String]’) -------> true; alert(Object.prototype.toString.call(2) === ‘[object Number]’) -------> true; alert(Object.prototype.toString.call([]) === ‘[object Array]’) -------> true; alert(Object.prototype.toString.call(new Date()) === ‘[object Date]’) -------> true;
alert(Object.prototype.toString.call(new RegExp()) === ‘[object RegExp]’) -------> true;
alert(Object.prototype.toString.call(null) === ‘[object Null]’) -------> true;
alert(Object.prototype.toString.call(e) === ‘[object Function]’) -------> true;
alert(Object.prototype.toString.call(f) === ‘[object Function]’) -------> true; 大小写不能写错,比较麻烦,但胜在通用。
4. constructor
undefined和null没有contructor属性
var bool = true
var num = 1
var str = 'abc'
var und = undefined
var nul = null
var arr = [1,2,3]
var obj = {name:'haoxl',age:18}
var fun = function(){console.log('I am a function')}
function Person(){}
var per = new Person()
function Student(){}
Student.prototype = new Person()
var haoxl = new Student()
console.log(bool.constructor === Boolean);// true console.log(num.constructor === Number);// true console.log(str.constructor === String);// true console.log(arr.constructor === Array);// true
console.log(arr.constructor === Object);// false
console.log(obj.constructor === Object);// true console.log(fun.constructor === Function);// true console.log(haoxl.constructor === Student);// false console.log(haoxl.constructor === Person);// true
constructor不能判断undefined和null,并且使用它是不安全的,因为contructor的指向是可以改变的
alert(c.constructor === Array) ----------> true alert(d.constructor === Date) -----------> true alert(e.constructor === Function) -------> true 注意: constructor 在类继承时会出错 eg: function A(){}; function B(){}; A.prototype = new B(); //A继承自B var aObj = new A(); alert(aobj.constructor === B) -----------> true; alert(aobj.constructor === A) -----------> false; 而instanceof方法不会出现该问题,对象直接继承和间接继承的都会报true: alert(aobj instanceof B) ----------------> true; alert(aobj instanceof B) ----------------> true; 言归正传,解决construtor的问题通常是让对象的constructor手动指向自己: aobj.constructor = A; //将自己的类赋值给对象的constructor属性 alert(aobj.constructor === A) -----------> true; alert(aobj.constructor === B) -----------> false; //基类不会报true了;
5. 无敌万能的方法:jQuery.type()
如果对象是undefined或null,则返回相应的“undefined”或“null”。 jQuery.type( undefined ) === "undefined" jQuery.type() === "undefined" jQuery.type( window.notDefined ) === "undefined" jQuery.type( null ) === "null" 如果对象有一个内部的[[Class]]和一个浏览器的内置对象的 [[Class]] 相同,我们返回相应的 [[Class]] 名字。 (有关此技术的更多细节。 ) jQuery.type( true ) === "boolean" jQuery.type( 3 ) === "number" jQuery.type( "test" ) === "string" jQuery.type( function(){} ) === "function" jQuery.type( [] ) === "array" jQuery.type( new Date() ) === "date" jQuery.type( new Error() ) === "error" // as of jQuery 1.9 jQuery.type( /test/ ) === "regexp" 其他一切都将返回它的类型“object”。
6. Array.isArray()
可以判断是否是数组类型
Array.isArray([1, 2, 3]); // true Array.isArray({foo: 123}); // false Array.isArray('foobar'); // false Array.isArray(undefined); // false
十一、浏览器页面渲染机制
1) 浏览器会解析三个东西:
- 一是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。
- 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。
- 三是Javascript脚本,等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。
2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。
- Rendering Tree 渲染树并不等同于DOM树,渲染树只会包括需要显示的节点和这些节点的样式信息。
- CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加到Rendering Tree上的每个Element(也就是每个Frame)。
- 然后,计算每个Frame 的位置,这又叫layout和reflow过程。
3)最后通过调用操作系统Native GUI的API绘制
这里重要要说两个概念,一个是Reflow,另一个是Repaint
重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
如何减少回流、重绘
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
性能优化策略
基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
JS优化: <script> 标签加上 defer属性 和 async属性用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。 defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。 async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
CSS优化: <link> 标签的 rel属性中的属性值设置为preload能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能
总结
综上所述,我们得出这样的结论:
浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。
————————————————
版权声明:本文为CSDN博主「十年呵护」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zzhuan_1/article/details/90199891
十二、实现深克隆
1 递归
function deepClone(obj) { // 过滤特殊情况 if(obj === null) { return null; } if(typeof obj !== "object") { return obj; } if(obj instanceof RegExp) { return new RegExp(obj); } if(obj instanceof Date) { return new Date(obj); } // 不直接创建空对象,目的是克隆的结果和之前保持相同的所属类 let newObj = new obj.constructor; for(let key in obj) { if(obj.hasOwnProperty(key)) { newObj[key] = deepClone(obj[key]); } } }
2 JSON.parse(JSON.stringfy(obj))
十三、原型 继承 prototype __proto__
每个JS对象一定对应一个原型对象,并从原型对象继承属性和方法
对象__proto__
属性的值就是它所对应的原型对象:
var one = {x: 1}; var two = new Object(); one.__proto__ === Object.prototype // true two.__proto__ === Object.prototype // true one.toString === one.__proto__.toString // true
prototype
首先来说说prototype
属性,不像每个对象都有__proto__
属性来标识自己所继承的原型,只有函数才有prototype
属性。
为什么只有函数才有prototype
属性?ES规范就这么定的。
开玩笑了,其实函数在JS中真的很特殊,是所谓的_一等公民_。JS不像其它面向对象的语言,它没有类(class
,ES6引进了这个关键字,但更多是语法糖)的概念。JS通过函数来模拟类。
当你创建函数时,JS会为这个函数自动添加prototype
属性,值是空对象 值是一个有 constructor 属性的对象,不是空对象。而一旦你把这个函数当作构造函数(constructor
)调用(即通过new
关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype
的所有属性和方法(实例通过设置自己的__proto__
指向承构造函数的prototype
来实现这种继承)。
小结
虽然对不熟悉的人来说还有点绕,但JS正是通过__proto__
和prototype
的合作实现了原型链,以及对象的继承。
构造函数,通过prototype
来存储要共享的属性和方法,也可以设置prototype
指向现存的对象来继承该对象。
对象的__proto__
指向自己构造函数的prototype
。obj.__proto__.__proto__...
的原型链由此产生,包括我们的操作符instanceof
正是通过探测obj.__proto__.__proto__... === Constructor.prototype
来验证obj
是否是Constructor
的实例。
回到开头的代码,two = new Object()
中Object
是构造函数,所以two.__proto__
就是Object.prototype
。至于one
,ES规范定义对象字面量的原型就是Object.prototype
。
更深一步的探讨
我们知道JS是单继承的,Object.prototype
是原型链的顶端,所有对象从它继承了包括toString
等等方法和属性。
Object
本身是构造函数,继承了Function.prototype
;Function
也是对象,继承了Object.prototype
。这里就有一个_鸡和蛋_的问题:
Object instanceof Function // true
Function instanceof Object // true
原文链接:https://github.com/creeperyang/blog/issues/9
十四、定时器的开启和关闭
// 当执行test时,如果flag为true,则启动定时器,定时验证uuip,当uuip为1时关闭定时器 let uuipTimers = null function listenerUUip() { clearTimeout(uuipTimers) let uuip = window.localStorage.getItem("uuipFlag") if(uuip == 1) { clearTimeout(uuipTimers) window.localStorage.removeItem('uuipFlag'); let wrap = document.getElementById('myIframeWrap') document.body.removeChild(wrap) } else{ uuipTimers = setTimeout(listenerUUip,2000) } } function test() { if(flag) { listenerUUip() } }
十五、Javascript 遍历中异步操作
function Pro(i) { return new Promise(function (resolve, reject) { // 一段耗时间的代码,这里用定时器代替 setTimeout(function () { // resolve 代表成功了,可以执行下面的代码了 resolve(`正在执行第${i}遍`); }, 1000) }) } async function getData() { for (let i = 0; i < 20; i++) { let res = await Pro(i); console.log(res); } } getData()
十六、把数组中指定的一个元素移动到第一位
arr.unshift(...arr.splice(arr.findIndex(i => i === key), 1))
十七、js中!、!!、.?、??、??=、的用法和含义
1) !和 !! 的区别用法
js中!的用法是比较灵活的,他除了做逻辑运算常常会用!做类型判断,还可以用!与上对象来求得一个布尔值
1、!可将变量转换成boolean类型,null、undefined和空字符串取反都为true,其余都为false
!null = true; !undefined = true !'' = true !'aa' = false
2、!! 是将表达式强制转化为boolean值运算,运算结果为true或者false,表达式是什么值,结果就是对应的bool值,不再取非,不是取非再取非的意思
!!false = false; !!'false' = true !!true = true !!(NaN || undefined || null || 0) = false var o = {flag: true}; var test = !!o.flag 等价于 var test = o.flag || false
由于对null和undefined 用!操作符时都会产生true的结果,所以用两个感叹号的作用就在于,如果明确设置了o中flag的值(非undefined/null/0)自然test就会取跟o.flag一样的值;如果没有设置,test就会默认为false,而不是null、undefined
2) 可选链(.?)
如果一个值为null、或者是undefined.那么我们再去用点操作符去调用一个方法或者访问一个属性会发生什么?
下面展示一些 内联代码片
。
let a;
let b = a.name;
如果是上面的这样的代码,那么我们能得到一个报错
只有当a存在的时候,我才会去访问a的name属性,如果你想再进一步处理,还可以继续判断以下a的数据类型.可是这不是我们今天的重点,就不多讲了.我们可以看到,这样一个简单的逻辑,我们就要写这么多的东西,那么有没有简单的写法呢?看下面的例子
let a;
let b = a?.name;
我们终于看到.?这个东西了,其实这个就叫做可选链,表达的意思,就和刚才if else的例子是一个意思,只有当a存在,同时a具有name属性的时候,才会把值赋给b,否则就会将undefined赋值给b.重要的是,不管a存在与否,这么做都不会报错.
当然我们还可以这么干
let a; let b; b = a?.name?.age?.haha?.就是不报错 a?.b?.c(“还是不报错”)
3) 空值合并运算符(??)
提到可选链了,再提一下空值合并运算符(??)空值合并运算符(??)是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
const foo = null ?? 'default string'; console.log(foo); // expected output: "default string" const baz = 0 ?? 42; console.log(baz); // expected output: 0
'hello world' ?? 'hi' // 'hello world'
'' ?? 'hi' // ''
false ?? 'hi' // false
null ?? 'hi' // 'hi'
undefined ?? 'hi' // 'hi'