1。 JavaScript的『预解释』与『变量提升』
先看以下代码输出啥?
var a= 1; function f() { console.log(a); var a = 2; } f();
首先答案是:undefined;
- 读取var a后,在当前作用域中查找是否有相同声明,如果没有就在当前作用域集合中创建一个名为a的变量,否则忽略此声明继续进行解析;
- 接下来,V8引擎会处理a = 2的赋值操作,首先会询问当前作用域中是否有名为a的变量,如果有进行赋值,否则继续向上级作用域询问
所以该题目中:在函数fn的作用域中,首先提取变量声明:var a;因为该作用域中有对a的赋值,所以不会继续查找上级作用域。且在赋值前打印,所以是undefined;
类似的看下题:
var a= 1; function f() { console.log(a); } f(); // 1
var a= 1; function f() { console.log(a); var a; } f(); //undefined
函数声明与函数表达式
我们看到,在编译器处理阶段,除了被var
声明的变量会有变量提升这一特性之外,函数也会产生这一特性,但是函数声明与函数表达式两种范式创建的函数却表现出不同的结果.
f(); g(); //函数声明 function f() { console.log('f'); } //函数表达式 var g = function() { console.log('g'); };
//f
//报错:VM693:2 Uncaught TypeError: g is not a function
f() 好理解属于函数声明提升;但是对于函数表达式g,被赋予undefined,undefeated无法被执行而报错。
冲突处理
变量之间冲突
var a = 3; var a = 4; console.log(a); //4
函数冲突
f(); function f() { console.log('f'); } function f () { console.log('g'); }; // g
3.函数与变量之间冲突
console.log(f); function f() { console.log('f'); } var f ='g';
ƒ f() {
console.log('f');
}
说明函数覆盖了变量;
类似的let,存在暂时性死区:
function f() { console.log(a); let a = 2; } f();
报错: //ReferenceError: a is not defined
这段代码直接报错显示未定义,let
与const
拥有类似的特性,阻止了变量提升,当代码执行到console.log(a)
时,执行换将中a
还从未被定义,因此产生了错误
======================
JS的执行机制:
https://www.cxymsg.com/guide/mechanism.html#javascript%E7%9A%84%E6%89%A7%E8%A1%8C%E7%8E%AF%E5%A2%83
谈谈你对原型链的理解?✨
这个问题关键在于两个点,一个是原型对象是什么,另一个是原型链是如何形成的
#原型对象
绝大部分的函数(少数内建函数除外)都有一个prototype
属性,这个属性是原型对象用来创建新对象实例,而所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性。
例如hasOwnProperty()
方法存在于Obejct原型对象中,它便可以被任何对象当做自己的方法使用.
用法:
object.hasOwnProperty( propertyName )
hasOwnProperty()
函数的返回值为Boolean
类型。如果对象object
具有名称为propertyName
的属性,则返回true
,否则返回false
。
var person = { name: "Messi", age: 29, profession: "football player" }; console.log(person.hasOwnProperty("name")); //true console.log(person.hasOwnProperty("hasOwnProperty")); //false console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true
由以上代码可知,hasOwnProperty()
并不存在于person
对象中,但是person
依然可以拥有此方法.
所以person
对象是如何找到Object
对象中的方法的呢?靠的是原型链。]
JavaScript的参数是按照什么方式传递的?
基本类型传递方式
由于js中存在复杂类型和基本类型,对于基本类型而言,是按值传递的.
var a = 1; function test(x) { x = 10; console.log(x); } test(a); console.log(a);
结果是: 10---1
虽然在函数test
中a
被修改,并没有有影响到 外部a
的值,基本类型是按值传递的.
#复杂类型按引用传递?
我们将外部a
作为一个对象传入test
函数.
var a = { a: 1, b: 2 }; function test(x) { x.a = 10; console.log(x); } test(a); console.log(a);
// { a: 10, b: 2 }
// { a: 10, b: 2 }
可以看到,在函数体内被修改的a
对象也同时影响到了外部的a
对象,可见复杂类型是按引用传递的.
可是如果再做一个实验:
var a = { a: 1, b: 2 }; function test(x) { x = 10; console.log(x); } test(a); console.log(a);
结果是:
//10
// { a: 1, b: 2 }
外部的a
并没有被修改,如果是按引用传递的话,由于共享同一个堆内存,a
在外部也会表现为10
才对. 此时的复杂类型同时表现出了按值传递
和按引用传递
的特性.
#按共享传递
复杂类型之所以会产生这种特性,原因就是在传递过程中,对象a
先产生了一个副本a
,这个副本a
并不是深克隆得到的副本a
,副本a
地址同样指向对象a
指向的堆内存.
因此在函数体中修改x=10
只是修改了副本a
,a
对象没有变化. 但是如果修改了x.a=10
是修改了两者指向的同一堆内存,此时对象a
也会受到影响.
有人讲这种特性叫做传递引用,也有一种说法叫做按共享传递
引用计次
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1
声明一个对象A,每多一个引用,A引用次数+1,每少一个引用,A的引用次数-1
缺点:相互引用的无法消除
标记清除
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。内存不能释放。
当变量离开环境时,则将其标记为“离开环境”。释放变量,回收内存。
类似于,函数执行顺序中,各个变量和函数执行在函数调用栈中,此时有标记,当执行完毕之后,退出调用栈,则消除标记,所以垃圾回收机制在一定时间内回收没有标记的变量
==========================
浏览器如何解析css选择器
浏览器会『从右往左』解析CSS选择器。
我们知道DOM Tree与Style Rules合成为 Render Tree,实际上是需要将Style Rules附着到DOM Tree上,因此需要根据选择器提供的信息对DOM Tree进行遍历,才能将样式附着到对应的DOM元素上。
以下这段css为例
.mod-nav h3 span {font-size: 16px;}
我们对应的DOM Tree 如下
若从左向右的匹配,过程是:
- 从 .mod-nav 开始,遍历子节点 header 和子节点 div
- 然后各自向子节点遍历。在右侧 div 的分支中
- 最后遍历到叶子节点 a ,发现不符合规则,需要回溯到 ul 节点,再遍历下一个 li-a,一颗DOM树的节点动不动上千,这种效率很低。
如果从右至左的匹配:
- 先找到所有的最右节点 span,对于每一个 span,向上寻找节点 h3
- 由 h3再向上寻找 class="mod-nav" 的节点
- 最后找到根元素 html 则结束这个分支的遍历。
后者匹配性能更好,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点);而从左向右的匹配规则的性能都浪费在了失败的查找上面
浏览器重绘与重排的区别?
- 重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素
- 重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变
『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』
即大小,位置等变化会带来重排;颜色等变化会导致重绘;
如何触发重排和重绘?
任何改变用来构建渲染树的信息都会导致一次重排或重绘:
- 添加、删除、更新DOM节点
- 通过display: none隐藏一个DOM节点-触发重排和重绘
- 通过visibility: hidden隐藏一个DOM节点-只触发重绘,因为没有几何变化
- 移动或者给页面中的DOM节点添加动画
- 添加一个样式表,调整样式属性
- 用户行为,例如调整窗口大小,改变字号,或者滚动。
#如何避免重绘或者重排?
#集中改变样式
我们往往通过改变class的方式来集中改变样式
// 判断是否是黑色系样式 const theme = isDark ? 'dark' : 'light' // 根据判断来设置不同的class ele.setAttribute('className', theme)
使用DocumentFragment
我们可以通过createDocumentFragment创建一个游离于DOM树之外的节点,然后在此节点上批量操作,最后插入DOM树中,因此只触发一次重排
var fragment = document.createDocumentFragment(); for (let i = 0;i<10;i++){ let node = document.createElement("p"); node.innerHTML = i; fragment.appendChild(node); } document.body.appendChild(fragment);
DOM的事件流是什么
事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点。
<!DOCTYPE HTML> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <body> <div></div> </body> </html>
如果单击了页面中的<div>
元素,那么这个click事件沿DOM树向上传播,在每一级节点上都会发生,按照如下顺序传播:
- div
- body
- html
- document
#事件捕获
事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前就捕获它。
还是以上一节的html结构为例:
在事件捕获过程中,document对象首先接收到click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即<div>
元素
- document
- html
- body
- div
事件流又称为事件传播,DOM2级事件规定的事件流包括三个阶段:事件捕获阶段(capture phase)、处于目标阶段(target phase)和事件冒泡阶段(bubbling phase)。
触发顺序通常为
- 进行事件捕获,为截获事件提供了机会
- 实际的目标接收到事件
- 冒泡阶段,可以在这个阶段对事件做出响应
什么是事件委托
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件.
在绑定大量事件的时候往往选择事件委托。
<ul id="parent"> <li class="child">one</li> <li class="child">two</li> <li class="child">three</li> ... </ul> <script type="text/javascript"> //父元素 var dom= document.getElementById('parent'); //父元素绑定事件,代理子元素的点击事件 dom.onclick= function(event) { var event= event || window.event; var curTarget= event.target || event.srcElement; if (curTarget.tagName.toLowerCase() == 'li') { //事件处理 } } </script>
优点:
- 节省内存占用,减少事件注册
- 新增子对象时无需再次对其绑定事件,适合动态添加元素
局限性:
- focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托
- mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,不适合事件委托
vue中央事件总线eventBus的简单理解和使用
实现instanceOf
// 模拟 instanceof function instance_of(L, R) { //L 表示左表达式,R 表示右表达式 var O = R.prototype; // 取 R 的显示原型 L = L.__proto__; // 取 L 的隐式原型 while (true) { if (L === null) return false; if (O === L) // 这里重点:当 O 严格等于 L 时,返回 true return true; L = L.__proto__; //L在上面已经等于了其隐式原型,即父级的显示原型,所以这里相当于L往上走了一级 } }
js中new一个对象的过程
function Person(name, age) { this.name = name; this.age = age; } var person = new Person("Alice", 23);
new一个对象的四个过程:
1、创建一个空对象 var obj = new Object();
2、让Person中的this指向obj,并执行Person的函数体 var result = Person.call(obj);
3、设置原型链,将obj的__proto__成员指向了Person函数对象的prototype成员对象 obj.__proto__ = Person.prototype;
4、判断Person的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。
if (typeof(result) == "object") person = result; else person = obj;
es5和es6实现类的继承 : https://www.cnblogs.com/xiaozhumaopao/p/11624344.html
如何实现一个Event
React/Vue不同组件之间是怎么通信的?
首先看一下es6中规定的Map的用法:
1,js创建map对象
var map = new Map();
2.将键值对放入map对象
map.set("key",value)
map.set("key1",value1)
map.set("key2",value2)
3.根据key获取map值
map.get(key)
4.删除map指定对象
delete map[key]
5.循环遍历map
map.
forEach(function(key){
console.log("key",key) //输出的是map中的value值
})
------------------
Vue
- 父子组件用Props通信
- 非父子组件用Event Bus通信
- 如果项目够复杂,可能需要Vuex等全局状态管理库通信
$dispatch
(已经废除)和$broadcast
(已经废除)
React
- 父子组件,父->子直接用Props,子->父用callback回调
- 非父子组件,用发布订阅模式的Event模块
- 项目复杂的话用Redux、Mobx等全局状态管理管库
- 用新的Context Api
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <div class="bottom">bottom</div> </body> <script> class EventEmeitter { constructor() { this._events = this._events || new Map(); // 储存事件/回调键值对 this._maxListeners = this._maxListeners || 10; // 设立监听上限 } } // 触发名为type的事件 EventEmeitter.prototype.emit = function (type, ...args) { //type--arson //...args---low-end let handler; // 从储存事件键值对的this._events中获取对应事件回调函数 handler = this._events.get(type); //console.log(handler); if (args.length > 0) { handler.apply(this, args); } else { handler.call(this); } return true; }; // 监听名为type的事件 EventEmeitter.prototype.addListener = function (type, fn) { // 将type事件以及对应的fn函数放入this._events中储存 if (!this._events.get(type)) { this._events.set(type, fn); } }; const emitter = new EventEmeitter(); // 监听一个名为arson的事件对应一个回调函数 emitter.addListener('arson', man => { console.log(`expel ${man}`); }); var bottom = document.querySelector('.bottom'); bottom.addEventListener('click', function () { // 我们触发arson事件,发现回调成功执行 emitter.emit('arson', 'low-end'); }) </script> </html>