目录
一、JSX 本质
const element = <h1>Hello, world!</h1>;
上述代码在 JS 代码中直接写 HTML(XML) 标记,即为 JSX。JSX 的本质是动态创建组件的语法糖。
所谓的语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
所谓的动态可以理解为借助 DOM API 动态创建组件,如 document.creatElement()
, 当然也可以使用 React 提供的相关 API 动态创建。所以实际开发中 JSX 不是必须使用的,但是建议使用。如下代码。
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
//借助 React 动态创建等价代码
const element = React.createElement(
'h1',//标签
{className: 'greeting'},//属性
'Hello, world!'//内容
);
JSX 本身是一个表达式,编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。也就是说可以将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX 等。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 的优点:
- 直观:声明式创建界面
- 灵活:代码动态创建界面
- 同其它前端框架相比无需学习新的模板语言
二、前端三大框架
参考
三大前端技术分别为:Angular、React、Vue
Angular:模板功能强大丰富,自带了极其丰富的angular指令。是一个比较完善的前端框架, 适用于大型项目。
Vue:构建数据驱动的Web界面的库,准确来说不是一个框架,它聚焦在V(view)视图层。
React: React 拥有较高的性能,代码逻辑非常简单。对模块化友好,但React 本身只是停留在视图层面,并不是一个完整的框架,所以如果是大型项目想要一套完整的框架的话,基本都需要加上
React Router
和Flux 或redux 或 mobox
才能写大型应用,所谓的 React 全家桶一般指React
、redux
、React Router
。
三、React 全家桶
所谓的 React 全家桶一般指 React
、Flux 或redux 或 mobox
、React Router
。
- Flux 或redux 或 mobox:主要做视图和数据的双向绑定。通过 Store 统一管理数据,所有组件数据更新都和 Store 直接通信,Store 类似中间者。
- React Router:主要用于实现路由,但是此路由不仅仅只是做页面切换,同时也能更好地组织代码(类似iOS原生的组件化)。
四、前端为何需要状态管理库(Flux 或redux 或 mobox)
在没有引入状态管理库时,组件间的通信需要一层一层传递下去。引入状态管理库后,STORE 相当去与一个中介者,所有的组件都会同 STORE 通信,从而减少了组件之间的通信。
五、高阶组件
高阶组件一般自身不涉及 UI 渲染,高阶组件的重心是实现的相关逻辑功能。高阶组件一般接受组件作为参数,然后返回新的组件。
//高阶组件实现代码
import React from "react";
export default function withTimer(WrappedComponent) {
return class extends React.Component {
state = { time: new Date() };
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
time: new Date()
});
}
render() {
return <WrappedComponent time={this.state.time} {...this.props} />;
}
};
}
使用的时候直接导出包装后组件 export default withTimer(ChatApp);
,同时该组件携带了高阶组件函数中的 time 属性
//高阶组件使用
import React from "react";
import withTimer from "../c06/withTimer";
export class ChatApp extends React.Component {
render() {
return (
<div>
<h2>{this.props.time.toLocaleString()}</h2>//携带了高阶组件的time属性
</div>
);
}
}
//导出withTimer(ChatApp)
export default withTimer(ChatApp);
六、函数作为子组件
函数作为子组件的最大特点是一个组件如何 render 它的内容,很大一部分可以由使用它的人来决定。核心点在于this.props.children
class MyView extends React.Component{
render() {
return (
<div>
{this.props.children('red')}//这里也可以不给子节点传递参数
</div>
)
}
}
<MyView>
//父节点传递参数的写法
{(color)=>{
<div style={{backgroundColor:color}}></div>
}}
//父节点没传参数的写法
//{<div></div>}
</MyView>
七、React Context API
Context API 主要用于解决组件间通信问题,根节点提供数据,子节点使用数据。如下代码是一个切换中英文语言的 demo
Provider 相关代码
const enStrings = {
submit: "Submit",
cancel: "Cancel"
};
const cnStrings = {
submit: "提交",
cancel: "取消"
};
const LocaleContext = React.createContext(enStrings);
class LocaleProvider extends React.Component {
state = { locale: cnStrings };
toggleLocale = () => {
const locale =
this.state.locale === enStrings
? cnStrings
: enStrings;
this.setState({ locale });
};
render() {
return (
<LocaleContext.Provider value={this.state.locale}>
<button onClick={this.toggleLocale}>
切换语言
</button>
{this.props.children}
</LocaleContext.Provider>
);
}
}
Consumer 相关代码
class LocaledButtons extends React.Component {
render() {
return (
<LocaleContext.Consumer>
{locale => (
<div>
<button>{locale.cancel}</button>
<button>{locale.submit}</button>
</div>
)}
</LocaleContext.Consumer>
);
}
}
调用
export default () => (
<div>
<LocaleProvider>
<div>
<br />
<LocaledButtons />
</div>
</LocaleProvider>
<LocaledButtons />
</div>
);
八、Flexbox 和 iOS 原生布局
Flexbox 布局参考
虽然 Masonry 和 SnapKit 能够简化布局写法,但和前端的布局思路相比,Auto Layout 的布局思路还处在处理两个视图之间关系的初级阶段,而前端的 Flexbox 已经进化到处理一组堆栈视图关系的地步了。
苹果公司也意识到了这一点,于是借鉴 Flexbox 的思路创造了 UIStackView,来简化一组堆栈视图之间的关系。UIStackView 虽然在布局思路上,做到了和 Flexbox 对齐,但写法上还是不够直观。前端布局通过 HTML + CSS 组合,增强了界面布局的可读性。
SwiftUI 在写法上非常简洁,可读性也很强。除了支持简洁的链式调用外,它还通过 DSL 定制了 UIStackView 的语法。这套 DSL 的实现,使用的是 Function Builders 技术,可以让 DSL 得到编译器的支持。有了这样的能力,可以说苹果公司未来可能会诞生出更多编译器支持的特定领域 DSL。DSL 编写后的处理方式分为两种:
- 通过解析将其转化成语言本来的面目,SwiftUI 使用的就是这种方式;
- 在运行时解释执行 DSL。SQL 就是在运行时解释执行的 DSL。
九、前端和iOS原生中的响应式框架
响应式框架到底是什么,为什么在 iOS 原生开发中没被广泛采用,却能在前端领域得到推广呢?
响应式框架指的是能够支持响应式编程范式的框架。使用了响应式框架,你在编程时就可以使用数据流传播数据的变化,响应这个数据流的计算模型会自动计算出新的值,将新的值通过数据流传给下一个响应的计算模型,如此反复下去,直到没有响应者为止。iOS 中有 RAC、RXSwift,前端中有 React.js。
JavaScript 每次操作 DOM 都会全部重新渲染。React.js 框架中引入了Virtual DOM 概念,页面组件状态会和 Virtual DOM 绑定,用来和 DOM(文档对象模型)做映射与转换。当组件状态更新时,Virtual DOM 就会进行 Diff 计算,最终只将需要渲染的节点进行实际 DOM 的渲染,并不是每一次都去渲染,相比传统的 JS 渲染在性能上有了很大的提升。如下图是虚拟DOM渲染原理。可以看出,操作 Virtual DOM 时并不会直接进行 DOM 渲染,而是在完成了 Diff 计算得到所有实际变化的节点后才会进行一次 DOM 操作,然后整体渲染。而 DOM 只要有操作就会进行整体渲染,避免了重复的整体渲染。正式因为如此React.js 具有很好的性能。
为什么 ReactiveCocoa 在 iOS 原生开发中就没流行起来呢?前端在有了 Virtual DOM 之后,整体渲染性能得到很大的提升。这种性能问题并不存在于 iOS 原生开发,Cocoa Touch 框架的界面节点树结构要比 DOM 树简单得多,没有前端那样的历史包袱。Cocoa Touch 每次更新视图时不会立刻进行整个视图节点树的重新渲染,而是会通过 setNeedsLayout
方法先标记该视图需要重新布局,直到绘图循环到这个视图节点时才开始调用 layoutSubviews
方法进行重新布局,最后再渲染。RAC、RXSwift 这样的框架并没有为 iOS 的 App 带来更好的性能。当一个框架可有可无,而且没有明显收益时,一般团队是没有理由去使用的。
九、虚拟 DOM
虚拟DOM其实就是一个JavaScript对象,是 React 最先提出来的概念。通过这个JavaScript对象来描述真实DOM。真实DOM的操作,一般都会对某块元素的整体重新渲染。采用虚拟DOM的话,当数据变化的时候,只需要局部刷新变化的位置就好了。
关于 DOM 的谣言
谣言 “DOM 操作慢,虚拟 DOM 快”。然而实际情况是:任何基于 DOM 的库(Vue/React) 都不可能在操作DOM 时比 原生DOM 快。
虚拟DOM存在的意义
Vue React 等框架给前端开发带来了革命性的变化。相比于此前的 jQuery 时代,它们的价值在于组件化、数据视图分离,数据驱动视图 (这才是核心)。数据视图分离,开发时只需要关注业务数据(React 的 state,Vue 的 data)即可,不用在实时的修改 DOM —— 这一点和 jQuery 有了本质区别。特别是对于大型的前端项目,将极大的降低开发复杂度,提高稳定性。而数据驱动视图,内部主要是借助虚拟dom实现。
Svelte 框架 不使用 vdom ,它将组件修改,编译为精准的 DOM 操作,主要是编译层面做了很多优化。和 React 设计思路完全不一样。但是目前还比较年轻,对应的组件库可能不是很丰富
虚拟DOM 在三方库中的优点
- 减少 DOM 操作:虚拟 DOM 可以将多次操作合并为一次操作、虚拟 DOM 借助 DOM diff 可以把多余的操作省掉
- 跨平台:虚拟 DOM 不仅可以变成 DOM,还可以变成小程序ios 应用、安卓应用,因为虚拟 DOM 本质上只是一个JS 对象
虚拟DOM缺点
需要通过函数创建
如何创建虚拟DOM
在 React 中,使用 createElement 函数来创建虚拟 DOM 节点:
React.createElement(
'div',
{className: 'red', onClick: () => React.createElement('span', {}, 'span1')},
'span2'
)
Vue 中(只能在render函数中得到 h函数) :
h('div', {class: 'red', on: {click: () => {}}}, [
h('span', {}, 'span1'),
h('span', {}, 'span2')
])
React 模板创建虚拟DOM,通过 babel 转为 creiteElement 形式
<div className="red"onClick={fn}>
<span>span1</span><span>span2</span></div>
Vue 模板创建虚拟DOM,通过 vue-loader 转为 h 形式
<div class="red"@click="fn">
<span>span1</span>
<span>span2</span>
</div>
react 和 vue 选择
中小公司通常选择vue,大公司选择react。vue 中默认使用虚拟dom diff 优化渲染;react 中虚拟 dom 可选,甚至可以不使用虚拟dom,直接调用 js 操作原生 dom,放弃react 自动的渲染,公司自定义优化空间更大。
dom diff 例子
例1:下图DOM 操作流程:
- DoM diff 发现标类型没变,只需要更新 div 对应的 DOM 的属性
-
子先素没变,不更新
例2:下图DOM 操作流程:
- div 没变,不用更新
- 子元素1标签没变,但是children变了,更新 DOM 内容(将hello 改成 world)
-
子元素2不见了,删除对应的 DOM(删除左图第二个 span 节点)
例2,如果从人类的视角来看,直接删除左图的第一个节点即可,一个步骤就能完成。但计算机只能从左往右以此做对比,所以在例2中需要两个步骤完成。如果想一步完成,可为每个span增加 key 值,这样计算机就知道是不是同一个节点,如果key值相同,说明是同一个节点,就不需要变动。此时一个步骤就能完成(即直接删除左图的hello节点)
dom 节点对比大概逻辑
Tree diff(树形差异对比)
- 将新旧两棵树逐层对比,识别出哪些节点需要更新。
- 如果节点是组件就看 Component diff
- 如果节点是标签就看 Element diff
Component diff(组件差异对比)
- 如果节点是组件,首先检查组件类型。
- 类型不同:直接替换(删除旧的)。
- 类型相同:只更新属性。
- 然后深入组件内部,递归进行 Tree diff。
Element diff(元素差异对比)
- 如果节点是原生标签,检查标签名。
- 标签名不同:直接替换。
- 标签名相同:只更新属性。
- 然后进入标签后代,递归进行 Tree diff。
dom diff 在代码逻辑中的表现
- dom diff 就是一个函数,我们称之为 patch
- patches = patch(oldVNode, newVNode),返回值 patches 就是要运行的 DOM 操作,
- patches 可能长这样
[
{type:'INSERT',vNode: xxx },//表示插入节点
{type:'TEXT'vNode :propsPatch: xxx}//表示更新文本节点
]
key 的作用
前面的例子2 ,dom diff 在同级比较需要两个步骤,实际是有bug的,但是通过设置 key 可以简化为一步操作。本质解决原因:通过 key,可以达到节点复用的目的,key 在 dom diff 过程中,可以标识是不是同一个节点,如果是同一个节点可以服用。
Vue2.0 v-for 中 :key 到底有什么用?
十、class 的本质
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
class Point {
constructor() {
}
toString() {
}
toValue() {
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
类的所有方法都定义在类的prototype属性上面。
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
在类的实例上面调用方法,其实就是调用原型上的方法。
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
十一、class 表达式
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name
码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。如果类的内部没用到的话,可以省略Me。const MyClass = class { /* ... */ };
十二、只能被继承,不能直接使用的类
class Shape {
constructor() {
//new.target 返回类名,如果是子类调用则返回子类名
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
十三、解构赋值与函数默认值结合使用
请问下面两种写法有什么差别?
// 写法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
十四、尾调用优化
十五、React Native 和 Flutter
目前主流的跨端方案,主要分为两种:
- 将 JavaScriptCore 引擎当作虚拟机的方案,代表框架是 React Native;
- 使用非 JavaScriptCore 虚拟机的方案,代表框架是 Flutter。
React Native 使用 JavaScript 语言来开发,Flutter 使用的是 Dart 语言。这两门编程语言,对 iOS 开发者来说都有一定的再学习成本。
React Native 框架的优势:
- JavaScript 的历史和流行程度都远超 Dart ,生态也更加完善,开发者也远多于 Dart 程序员。
- 从页面框架和自动化工具的角度来看,React Native 也要领先于 Flutter。这,主要得益于 Web 技术这么多年的积累,其工具链非常完善。
- 热更新技术
- 包体积小。Flutter 的渲染引擎是自研的,并没有用到系统的渲染,所以 App 包必然会大些。但是,我觉得从长远来看,网速越来越快,App Store 对包大小的限制只会越来越小,所以说这个问题一定不会成为卡点。
Flutter 的优势:
- 首先在于其性能。JS 在浏览器中解释执行,且无法针对移动端进行优化。Flutter 却不一样。它一开始就抛弃了历史包袱,使用全新的 Dart 语言编写,同时支持 AOT(预先编译) 和 JIT(实时编译) 两种编译方式,而没有采用 HTML/CSS/JavaScript 组合方式开发,在执行效率上明显高于 JavaScriptCore 。
- 重写了 UI 框架,从 UI 控件到渲染,全部重新实现了,依赖 Skia 图形库和系统图形绘制相关的接口,保证了不同平台上能有相同的体验。
十六、JavaScriptCore 简介
JavaScriptCore 框架的框架名是 JavaScriptCore.framework。JavaScriptCore 为原生编程语言 Objective-C、Swift 提供调用 JavaScript 程序的动态能力,还能为 JavaScript 提供调用原生的能力。正是因为这种桥梁作用,出现了很多使用 JavaScriptCore 开发 App 的框架 ,如 React Native、Weex、小程序、WebView Hybird 等。
JavaScriptCore 原本是 WebKit 中用来解释执行 JavaScript 代码的核心引擎。从 iOS7 开始,苹果公司将其作为系统级的框架提供给开发者使用。苹果公司有 JavaScriptCore 引擎、谷歌有 V8 引擎、Mozilla 有 SpiderMonkey。
JavaScriptCore 框架主要由 JSVirtualMachine 、JSContext、JSValue 类组成。
- JSVirturalMachine 的作用是为 JS 代码的运行提供一个虚拟机环境。在同一时间内,JSVirtualMachine 只能执行一个线程。每个 JSVirtualMachine 都有自己的 GC(Garbage Collector,垃圾回收器),以便进行内存管理,所以多个 JSVirtualMachine 之间的对象无法传递。
- JSContext 是 JS 运行环境的上下文,负责原生和 JS 的数据传递。
-
JSValue 是 JS 的值对象,用来记录 JS 的原始值,并提供进行原生值对象转换的接口方法。
下图是 JavaScriptCore 和原生应用的交互。每个 JavaScriptCore 中的 JSVirtualMachine 对应着一个原生线程,同一个 JSVirtualMachine 中可以使用 JSValue 与原生线程通信,遵循的是 JSExport 协议。JSExport 协议的主要作用是将原生类方法和属性提供给 JavaScriptCore 使用,JavaScriptCore 可以将 JSValue 提供给原生线程使用。
十七、React 生命周期
十八、let 和 var
作用域
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上述代码中变量 i 使用 var 声明,在全局范围内都有效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会发生改变,而循环内被赋给数组 a 的函数内部的 console.log(i)
,里面的 i 指向的就是全局的 i。也就是说,所有的 i,指向的都是同一个 i,导致运行时输出的是最后一轮的 i 的值,也就是 10。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
变量提升
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
块级作用域
ES5 只有全局作用域和函数作用域,没有块级作用域。内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined(原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。)
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
const定义常量,初始化要设置默认值,否则会报错。
十九、RN 和 原生通信原理
二十、数据类型
JavaScript拥有 7 种数据类型,分别是:
- 1.数值(Number),整数或者浮点数;NaN(not a number,非数)属于Number数据类型,表示不可表示的值,并且NaN不等于任何数,与他自己也不相等。
- 2.字符串(String),'a','A','hello world!';
- 3.布尔值(boolean), True 或者 False;
- 4.null,表示空
- 5.undefined, 表示不存在
- 6.对象(object)
- 7.Symbol,表示独一无二的值。Symbol 值通过Symbol函数生成,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s1 = Symbol('foo');
s1 // Symbol(foo)
s1.toString() // "Symbol(foo)"
引用类型: Object,Array,RegExp,Date等
基本数据类型:字符串、数值、布尔、undefined、null
其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型。
<body>
<script>
// 包装类型
// const str = 'andy'
// console.log(str.length)
// 包装过程
// const str = new String('andy')
// str.substring()
// str = null
</script>
</body>
Symbol 类型
- symbol值都是唯一的,一个symbol值能作为对象属性的标识符
- 避免被重复的算法所覆盖对象的key,场景可以参考计算id
- Object.getOwnPropertySymbols可以迭代对象上的该属性
let smb0 = Symbol('abc');
let smb1 = Symbol('abc'); // 还是新创建的一个
console.log(smb0 === smb1); // false
console.log('===============');
// Symbol.for 简单理解为一个复用池
let smb2 = Symbol.for('abc'); // 对全局进行注册并保存
let smb3 = Symbol.for('abc'); // 从全局注册中获取 console.log(smb3 === smb2); // true console.log('===============');// true
应用场景
使用Symbol作为属性名,保证对象中不会出现同名属性。三种添加属性的方式:
let mySymbol = Symbol();
let obj = {}
obj[mySymbol] = 'hello';// 第一种添加属性的方式
console.log(obj[mySymbol]);
let mySymbol = Symbol();
let obj = {
[mySymbol]: 'world' // 注意mySymbol必须加上方括号,否则为字符串而不是Symbol类型。
}
console.log(obj[mySymbol]);
let mySymbol = Symbol();
let obj = {};
Object.defineProperty(obj, mySymbol, {
value: '你好'
})
console.log(obj[mySymbol]);
二十一、滚动视图性能提升
- renderItem中的组件最好是无状态组件尽量不包含 state。
- 使用 keyExtractor 指定一个key,不要使用索引,尤其是存在列表的增删的情况下。
- 使用合适的 size 避免屏幕外渲染。
二十二、undefined 和 null
根据语义来区分,null 更多的表示引用语义 ,而 undefined 更多的表示值语义表示未定义。
null 表示一个值被定义了,定义为“空值”;
undefined 表示根本不存在定义;
所以设置一个值为 null 是合理的,如 : objA.valueA = null; 但设置一个值为 undefined 是不合理的
二十三、RN 中的坑
- 图片在 Android 上不能直接设置圆角,要用 View 包裹并设置 View 的圆角。
- 列表上拉加载官方有问题,会出现连续多次回调的问题,需要添加标记,防止重复调用问题。
- 图片控件无法添加子节点。但是
BackgroundImage
控件可以添加子节点 - 图片在 iOS 端未实现沙盒缓存,只有内存缓存
- 同时多次调用原生方法时,存在一定概率丢失一些回调的情况
- 打包未剔除没有用到的库,导致打出来的包有很多无用代码
- 官方未实现渐变色的渲染功能
二十四、禁止侧滑返回手势
static navigationOptions = ({navigation})=>{
return {
gesturesEnabled: false,
};
};
二十五、监听 viewDidAppear
首先请升级 react-navigation 到最新版本,因为此监听方法是较新版本才更新出来的方法。参考
componentDidMount() {
// 添加监听
this.viewDidAppear = this.props.navigation.addListener(
'didFocus',
(obj)=>{
console.log(obj)
}
)
}
componentWillUnmount() {
// 移除监听
this.viewDidAppear.remove();
}
二十六、setState()
(1)除了 constructor,不能直接修改 state
除了constructor 方法之外,不能直接修改 state,否则会报错。constructor 是唯一可以直接修改 state 的地方。
(2)state 有 callBack 参数()
//更新state不一定是同步,可能是异步的
this.setState(
{focus: !focus},
() => {
if (this.state.focus) {
this.inputView.focus()
}
},
)
(3)state 的更新操作会被合并
fetchPosts().then(response => {
this.setState({
posts: response.posts
comments: response.comments
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
// 在执行完上述两个函数后,state中的comments值,我们无法知道是来自fetchPosts还是fetchComments
(4)state 中可以获取之前值的写法
//方式一
this.setState(
preState => ({
focus: !preState.focus,
})
)
//方式二
this.setState(({activeRowKey}) => ({
activeRowKey: null,
activeRowIndex: null,
releasedRowKey: activeRowKey,
scrollEnabled: this.props.scrollEnabled,
}));
二十七、JS 对象浅拷贝、深拷贝、赋值
数据类型
- 基本数据类型主要是:undefined,boolean,number,string,null。
- 引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。
- 对于==/===,基本类型是值比较,引用类型是引用比较
浅拷贝与深拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
var a1 = {b: {c: {}};
var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存
var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存
赋值、浅拷贝、深拷贝区别
- 赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
- 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
== 和 ===
== 比较会做类型转换
浅拷贝方法
- Object.assign()
- .函数库lodash的_.clone方法
- 展开运算符...
- Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
- Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
深拷贝方法
- JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
- 函数库lodash的_.cloneDeep方法
- jQuery.extend()方法
- 手写递归方法
二十八、props 和 state
相同点
- 不管是props还是state的改变,都会引发render的重新渲染
- 都能由自身组件的相应初始化函数设定初始值。
不同点
- 初始值来源:state的初始值来自于自身的getInitalState(constructor)函数;props来自于父组件或者自身getDefaultProps
- 修改方式:state 只能在自身组件中 setState,不能由父组件修改;props 只能由父组件修改,不能在自身组件修改。
- 对子组件:props 是一个父组件传递给子组件的数据流,这个数据流可以一直传递到子孙组件;state 代表的是一个组件内部自身的状态,只能在自身组件中存在。
二十九、shouldComponentUpdate
三十、props.children.map 函数异常提示
this.props.children 的值有三种可能:
- 1.当前组件没有子节点,它就是 undefined;
- 2.有一个子节点,数据类型是 object ;
- 3.有多个子节点,数据类型就是 array 。
可以使用系统提供的React.Children.map()
方法安全的遍历子节点对象
var NotesList = React.createClass({
render: function() {
return (
<ol>
{
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
}
</ol>
);
}
});
ReactDOM.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.body
);
三十一、code push 原理
code push 调用 react native 的打包命令,将当前环境的非 native 代码全量打包成一个 bundle 文件,然后上传到微软云服务器(Windows Azure)。 在 app 中启动页(或 splash 页)编写请求更新的代码(请求包含了本地版本,hashCode、appToken 等信息),微软服务端对比本地 js bundle 版本和微软服务器的版本,如果本地版本低,就下载新的 js bundle 下来后实现更新(code push 框架实现)。
三十二、React.cloneElement
三十三、shouldComponentUpdate
使用 shouldComponentUpdate() 以让 React 知道当前状态或属性的改变是否不影响组件的输出,默认返回 ture,返回 false 时不会重写 render,而且该方法并不会在初始化渲染或当使用 forceUpdate() 时被调用,我们要做的只是这样:
shouldComponentUpdate(nextProps, nextState) {
return nextState.someData !== this.state.someData
}
另外,还涉及一个深比较和浅比较的问题,可参考该篇文章。
三十四、JS 泛型
三十五、装饰器
三十六、组件 key 作用
{
[<Text key={1}>'aa'</Text>,<Text key={1}>'aa'</Text>,<Text key={1}>'aa'</Text>,<Text key={1}>'aa'</Text>]
}
比如有如上一段jsx代码在render方法中,如果用一样的key,结果只显示了一个text,如果默认不加key属性,那么会显示四个text。
key={new Date().getTime()}
flatList 本身有复用功能,每次重新赋值数据的时候,如果 cell 内部又相关的 state 状态,则 state 状态无法恢复到初始值(因为视图不会释放),此时可以 设置 flatList 的 key={new Date().getTime()}
保证每次切换数据源都重新刷新 flatList,内部的 cell 视图也会先释放再创建。
三十七、Pure Component 和 Component
三十八、js 中的浅比较和深比较
三十九、CSR和SSR
四十、Cookie,Sessionstorage,Localstorage的区别
Cookie、SessionStorage 和 LocalStorage 都是在客户端存储数据的方式,但它们之间有一些区别:
Cookie:
Cookies 是由服务器发送到浏览器并存储在浏览器上的小型文本文件。
Cookies 主要用于在客户端和服务器之间传递会话信息,如用户身份验证、跟踪用户行为等。
Cookies 在每次 HTTP 请求中都会被发送到服务器,包括图片、CSS 文件、JavaScript 文件等。
Cookies 有大小限制(通常为4KB)。
SessionStorage:
SessionStorage 中的数据在当前会话结束时(即浏览器关闭)会被清除。
SessionStorage 只能在当前会话中的同一个页面或标签页中访问,不能在不同页面或标签页之间共享数据。
LocalStorage:
LocalStorage 与 SessionStorage 类似,但数据存储在本地并且不会随会话结束而被清除。
LocalStorage 中的数据可以跨会话和跨浏览器窗口进行共享,直到被显式删除。
LocalStorage 拥有较大的存储容量限制(通常为5MB),比 Cookies 大得多。
总的来说,Cookie 主要用于在客户端和服务器之间传递数据,而 SessionStorage 和 LocalStorage 则主要用于在客户端存储数据。SessionStorage 用于会话级别的数据,而 LocalStorage 用于长期存储的数据。
四十一、浏览器跨域的原因,以及怎么解决
浏览器同源策略
作用:同源策略是浏览器的一种保护本地数据的机制 (即本网站的只能请求网站下的接口数据)
判断:网页打开时地址栏里地址,和内嵌Ajax请求ur1地址之间,协议,域名,端口是否相同。
非同源触发限制: 浏览器判断如果非同源,请求发送,虽然服务器响应了,但是浏览器无法接收。因为浏览器判断本次响应头没有开启 CORS 跨域资源共享机制,所以被拒绝了,于是丢弃响应的数据并在浏览器中报错。违反了同源策略,也叫做发生了垮域请求。
跨域
就是跨越域名访问,违反了同源策略,根本原因是浏览器对Ajax有同源隔离限制。
- 服务器和服务器之间不存在跨域问题
- html 中的script、img、link等对应的链接也不会存在跨域问题。
跨域请求解决方案
正常来说,后端的接口肯定在另外一个服务器(主机名)下,所以跨域问题很常见。解决方案如下:
- 方案1:前端+后端,支持jsonp方式请求 ->仅支持GET方式(用的少,了解即可)
- 方案2:只需要后端开启CORS跨域资源共享(就是在响应头里设置一个字段)
Access-Control-A11ow-origin:* 。此字段表示本服务器允许任何人来请求。流程:前端正常请求,浏览发现这个接口允许跨城,所以不报错拿到数据给前端用。 - 方案3:代理转发方案。加入请求三方服务,如第三方提供的新闻或天气接口,且三方服务不支持浏览器跨域请求(未设置Access-Control-A11ow-origin:)。此时就需要自己的服务器介入,充当代理转发,因为服务器和服务器之间不存在跨域问题。 本地自己nodethttp搭建一个web服务,用服务器去请求另外一个服务器的接口把数据请求回来转发给自己的前端使用。自己的服务器设置 Access-Control-A11ow-origin:。
jsonp 简介
JSONP
是客户端与服务端进行跨域通信比较常用的解决办法,它的特点是简单,兼容老式浏览器,并且对服务器影响小。
JSONP
的实现的实现思想可以分为两步:
- 第一:在网页中动态添加一个
script
标签,通过script
标签向服务器发送请求,在请求中会携带一个请求的callback
回调函数名。 - 第二: 服务器在接收到请求后,会进行相应处理,然后将参数放在
callback
回调函数中对应的位置,并将callback
回调函数通过json
格式进行返回。
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
window.onload = function () {
var btn = document.getElementById("btnLogin");
btn.addEventListener("click", function () {
sendRequest();
});
};
function sendRequest() {
var userName = document.getElementById("userName").value;
//请求参数,其中包含回调函数
var param = "name=" + userName + "&callback=successFn";
//请求的url
var url = "http://localhost:3000/getUserNameInfo?" + param;
var script = document.createElement("script");
script.src = url;
document.body.appendChild(script);
}
function successFn(result) {
console.log("result=", result);
}
</script>
</head>
<body>
用户名:<input type="text" id="userName" /> <br />
<button id="btnLogin">登录</button>
</body>
</html>
服务端代码。在服务的代码中,需要接收回调函数的名称。同时返回的内容中,包含了回调函数的名称,以及传递给该回调函数的具体数据。这样当回调函数返回给浏览器后,浏览器可以从全局的环境中查找该回调函数,并进行执行。
var express = require('express')
var app = express();
app.get('/getUserNameInfo', function (req, res) {
var userName = req.query.name;
//获取请求的回调函数
var callbackFn = req.query.callback
console.log('callbackFn==',callbackFn)
console.log('userName=',userName)
var result = {
id: 10001,
userName: userName,
userAge:21
};
var data = JSON.stringify(result);
res.writeHead(200, { 'Content-type': 'application/json' })
//返回值是对对回调函数的调用
res.write(callbackFn+'('+data+')')
// res.write(data);
res.end()
})
app.listen(3000, function () {
console.log('服务端启动....')
})
优缺点:
- 优点:简单,不存在浏览器兼容性的问题
- 缺点:只能实现
get
请求,如果是post
请求则无法进行跨域的处理。
四十二、截流和防抖
防抖
单位时间内,频繁触发事件,只执行最后一次。思路:每次出触发前先清空上一次的定时器。文本框内进行搜索提示逻辑就属于防抖。
<script>
// 利用防抖实现性能优化
//需求: 鼠标在盒子上移动,里面的数字就会变化 + 1
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
// 如果里面存在大量消耗性能的代码,比如dom操作,比如数据处理,可能造成卡顿
}
// box.addEventListener('mousemove', _.debounce(mouseMove, 500))
// 手写防抖函数
// 核心是利用 setTimeout定时器来实现
// 1. 声明定时器变量
// 2. 每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有先清除以前的定时器
// 3. 如果没有定时器,则开启定时器,存入到定时器变量里面
// 4. 定时器里面写函数调用
function debounce(fn, t) {
let timer
// return 返回一个匿名函数
return function () {
// 2.3.4
if (timer) clearTimeout(timer)
timer = setTimeout(function () {
fn() // 加小括号调用 fn函数
}, t)
}
}
box.addEventListener('mousemove', debounce(mouseMove, 500))
// debounce(mouseMove, 500) // 调用函数
// debounce(mouseMove, 500) = function () { 2.3.4}
</script>
截流
节流:单位时间内,频繁触发事件,只执行一次。思路:当前正在处于即将执行或执行时,就不再触发。
<script>
// 利用节流实现性能优化
//需求: 鼠标在盒子上移动,里面的数字就会变化 + 1
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
// 如果里面存在大量消耗性能的代码,比如dom操作,比如数据处理,可能造成卡顿
}
// box.addEventListener('mousemove', mouseMove)
// 利用lodash库实现节流 -
// 语法: _.throttle(fun, 时间)
// box.addEventListener('mousemove', _.throttle(mouseMove, 3000))
// 手写一个节流函数- 每隔 500ms + 1
// 节流的核心就是利用定时器(setTimeout) 来实现
// 1.声明一个定时器变量
// 2.当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器
// 3.如果没有定时器则开启定时器,记得存到变量里面
// 3.1定时器里面调用执行的函数
// 3.2定时器里面要把定时器清空
function throttle(fn, t) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(function () {
fn()
// 清空定时器
timer = null
}, t)
}
}
}
box.addEventListener('mousemove', throttle(mouseMove, 3000))
</script>
四十三、函数变量提升
在代码执行之前,JavaScript 引擎会将变量和函数声明提升到当前作用域的顶部
。但只有声明会被提升,而赋值不会被提升。举个例子,函数声明会被提升。
foo(); // 输出 "Hello, world!"
function foo() {
console.log("Hello, world!");
}
在这个例子中,即使 foo() 的调用在函数声明之前,JavaScript 引擎也会将函数 foo 的声明提升到作用域的顶部,因此函数可以在声明之前被调用。但是,需要注意的是函数表达式并不会被提升,只有函数声明会被提升。在这个例子中,由于 baz 是一个函数表达式而不是函数声明,因此在声明之前调用它会导致错误。
baz(); // 报错:baz is not a function
var baz = function() {
console.log("This is a function expression");
};
- 变量声明提升主要影响使用var声明的变量。let和const(ES6中引入的块级作用域变量)则不会经历同样的提升过程。
四十四、判断数据类型的方法,以及区别
typeof:typeof 是一个 JavaScript 运算符,用于确定给定变量的数据类型。它返回一个表示变量类型的字符串。
js复制代码typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof {}; // "object"
typeof []; // "object"(数组也是对象)
typeof null; // "object"(这是 typeof 的一个已知问题)
typeof undefined; // "undefined"
typeof function() {}; // "function"
instanceof:instanceof 操作符用于检查对象是否属于某个特定的对象类型。它用于检查对象的原型链。
js复制代码var arr = [];
arr instanceof Array; // true
arr instanceof Object; // true(因为数组也是对象)
Object.prototype.toString.call():这是一个通用的方法,可以返回准确的对象类型。通过调用 call 方法并传递要检查的值作为上下文,可以获取该值的内部 [[Class]] 属性,从而得到准确的类型。
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call("hello"); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(function() {}); // "[object Function]"
四十五、CSS中的> 和 &
.my-nav {
height: 44px;
position: fixed;
> a {
color: #999;
font-size: 14px;
line-height: 44px;
transition: all 0.3s;
&::after {
content: '';
transition: all 0.3s;
}
&.active {
color: #222;
&::after {
width: 14px;
}
}
}
表示选择器的直接子元素,例如.my-nav > a表示选择.my-nav下的直接子元素a。
- & 表示当前选择器本身,可以用来连接伪类或伪元素,例如&::after表示当前选择器本身的伪元素::after。
四十六、sass和less
- sass 优于 less,首选 sass。sass 生态好
- sass 对应的库为 sass,对应的文件名有sass、scss,一般使用scss,vue3 中script 标签上对应的关键字为 scss
- scss 在写法上比sass更舒适点
// sass嵌套写法
.div
.div2
// scss嵌套写法,相比sass更直观
.div {
.divv2 {
....
}
}
sass 常用语法
除了最常见的嵌套语法,还有如下部分语法
// 1、定义变量和使用
$primary-color: #3498db;
$padding: 15px;
.container {
color: $primary-color;
padding: $padding;
}
// 2、混合,可重复使用代码
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
.box { @include border-radius(10px); }
// 3、导入
@import 'partials/_variables';
@import 'partials/_mixins';
// 4、函数
@function em($pixels, $context: 16) {
@return #{$pixels / $context}em;
}
$font-size: em(14);
原生css定义变量
变量在CSS中通过两个短横线(--)来标识,可以定义在任何选择器中,并在文档的其它部分被使用。如下代码,在:root伪类或者任何其他选择器中定义变量。:root是一个伪类,代表文档的根元素,通常用来定义全局变量。
// 定义变量
:root {
--main-color: #3498db;
--padding-size: 15px;
}
// 使用变量
.container {
color: var(--main-color);
padding: var(--padding-size);
}
/* 变量默认值 */
.container {
color: var(--main-color, #666); /* 如果--main-color未定义,则使用#666 */
}
四十七、JS输入输出
输出语句
alert('你好,js')
// document.write 向页面文档输入内容 显示到页面body标签之内, 可以正常的解析标签
document.write('今日特价')
document.write('<h4>今日特价</h4>')
console.log('给咱们程序员使用的')
输入语句
let name = prompt('请输入您的姓名:')
四十八、模板字符串
// 1. 字符串拼接
let age = 91
console.log('pink老师今年' + age + '岁')
// 2. 模板字符串
console.log(`pink老师今年${age}岁`)
四十九、数据类型
基本数据类型: number、string、boolean、undifined、null
引用数据类型: object、function、array
通过typeof
关键字检测数据类型。
undefined 和 null
undefined
- 自动赋值:如果一个变量被声明了但没有被显式地赋予任何值,那么它的默认值就是undefined。
- 隐式存在:在很多情况下,如果尝试访问一个不存在的属性或方法,或者一个函数没有返回值(或没有执行到return语句),JavaScript会隐式地返回undefined。
- 不是原始值:虽然undefined看起来像是一个值,但实际上它是一个类型(undefined类型),并且它是这个类型的唯一值。
null
- 手动赋值:null是一个显式地表示“没有对象”的值。它是JavaScript的基本类型之一(尽管它表示的是一个空值,但它是一个特殊的类型,即Null类型)。
- 用途:null通常用于表示变量应该指向一个对象,但当前并不指向任何对象。这是一种明确的、显式的表示方式,告诉代码的其他部分“我知道这里应该有个对象,但现在没有”。
- 默认值:不同于undefined,null不是任何变量的默认值。你需要显式地将一个变量设置为null来表示该变量当前没有指向任何对象。
比较
- 相等性:undefined == null的结果是true,在JavaScript中,==(宽松相等)会将undefined和null视为等价,但===(严格相等)则不会,因为它们的类型不同。
- 用途区别:undefined通常用于表示变量未初始化或函数没有返回值的情况,而null则用于表示一个空对象引用。
两者都表示空,undefined 表示没设置值,null 表示设置的指是 null。
typeof null
typeof null的结果是一个字符串"object",这是 js 的 bug。
typeof undefined的结果是 undefined,属于正常。
五十、闭包
概念:一个函数对周围状态的引用捆绑在一起,闭包让开发者可以从内部函数访问外部函数的变量。
简单理解:闭包 = 内层函数 + 外层函数的变量
function createCounter() {
let count = 0; // 外部函数的局部变量
return {
increment: function() { // 内部函数
count = count + 1;
return count;
},
decrement: function() {
count = count - 1;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
counter.increment();
console.log(counter.getCount()); // 输出: 1
counter.decrement();
console.log(counter.getCount()); // 输出: 0
在这个例子中,createCounter函数返回了一个对象,这个对象包含了三个方法:increment、decrement和getCount。这三个方法都可以访问和修改createCounter函数内部的count变量。尽管createCounter函数执行完毕后,其执行环境(词法作用域)本应该被销毁,但由于闭包的存在,count变量得以保留,并且可以通过返回的对象中的方法被访问和修改。
重新整理
<script>
// 闭包:函数内部引用外部的变量 执行时,形成闭包
function fn() {
var b = new Array(200000) .fill( 'Green'); // 比
function inner() {
console.log(b);
}
debugger
inner();
}
fn( ) ;
</script>
上述代码执行到 inner() 时会形成闭包 inner,但是当 inner 函数执行完出栈,此时闭包会被销毁。
// 闭包:函数内部引用外部的变量 执行时,形成闭包
function fn() {
var b = new Array(200000) .fill( 'Green'); // 比
function inner() {
console.log(b);
}
debugger
inner();
rerurn inner
}
// 函数内部持有外部变量,而函数被引用没释放,会形成闭包
var fn = fn( ) ;
fn = null
代码之执行到var fn = fn( ) ; 会产生闭包 inner ,如果不执行 fn = null,则 inner闭包一直无法释放,产生内存泄漏。
五十一、var相比let的缺点
1、全局作用域和函数作用域:
- var 声明的变量具有函数作用域或全局作用域(在函数外部声明时)。这意味着变量可以在声明它的函数内部或全局范围内被访问和修改,这可能导致意外的变量覆盖或难以追踪的变量引用。
- 相反,let 声明的是块级作用域的变量,它只在声明它的块({}内)中有效。这有助于减少变量之间的冲突和意外覆盖,使得代码更加清晰和安全。
2、变量提升(Hoisting):
- 如前所述,var 声明的变量会被提升到其作用域的顶部,但仅提升声明,不提升初始化。这意味着你可以在声明之前访问变量(得到 undefined),这可能导致难以追踪的错误。
- let 和 const 也被提升,但它们在声明之前的区域内处于“暂时性死区”(Temporal Dead Zone, TDZ),尝试访问会抛出 ReferenceError。这有助于开发者在变量声明之前就知道不能访问它,从而避免了潜在的错误。
3、可重复声明:
- 在同一个作用域内,使用 var 可以多次声明同一个变量,后一次声明会覆盖前一次声明的值,这可能导致混淆和错误。
- let 和 const 不允许在同一作用域内重复声明同一个变量,这有助于避免变量名的混淆和意外的值覆盖。
4、在 for 循环中的行为:
- 在使用 var 的 for 循环中,循环变量实际上在循环外部的作用域中被声明。这可能导致在循环结束后,循环变量仍然保持其最后的值,并可能在循环外部被意外修改或访问。
- 使用 let 声明的循环变量具有块级作用域,它仅在循环块内部有效。每次迭代时,let 都会创建一个新的变量绑定,这意味着循环变量在循环外部是不可见的,有助于避免潜在的错误。
5、缺乏严格模式的一些特性:
- 在严格模式('use strict';)下,var 的一些不安全的特性(如自动将未声明的变量转换为全局变量)会被限制,但 let 和 const 本身就是为了提供更严格和更安全的变量声明方式而设计的。
6、变量容易被覆盖
var myname = " 极客时间 "
function showName(){
console.log(myname);
if(0){
var myname = " 极客邦 "
}
console.log(myname);
}
showName()
上面这个案例两个输出结果都是 undifined,执行后对应的堆栈内容如下图。myname 变量:一个在全局执行上下文中,其值是“极客时间”;另外一个在 showName 函数的执行上下文中,其值是 undefined。
五十二、ES6 是如何解决变量提升带来的缺陷
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
1、编译并创建执行上下文
- 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
- 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
-
在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中。
2、执行到代码块开始
其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。
3、执行到 console.log(a)
当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。变量查找过程参照箭头方向
4、离开代码块
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示。
五十三、js垃圾回收
引用计数
IE采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象
算法:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
- 如果减少一个引用就减1 --
- 如果引用次数是0 ,则释放内存
标记清除法
现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
- 3.那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
缺点:
- 可能会导致程序暂停,因为它需要遍历所有对象,且回收过程也可能比较耗时。
- 回收后释放的空间可能是不连续的,导致出现内存碎片,影响大对象的分配。为解决这个问题,一些GC策略会结合其他算法,如压缩(Compaction)来整理内存,减少碎片。
五十四、属性选择器
基本语法
CSS属性选择器的语法结构为:[attribute_name="attribute_value"]。其中,attribute_name 是要选择的元素的属性名,attribute_value 是该属性需要匹配的值(在某些情况下,这个值可以是部分匹配或特定格式的匹配)。
使用方式
1、存在选择器:选择所有具有指定属性的元素,不论其属性值为何。例如,[data-info] 会选择所有具有data-info属性的元素。vue中的 scoped 就是利用这个原理。
2、完全匹配选择器:选择属性值完全等于指定值的元素。例如,[href="https://www.example.com"] 会选择所有href属性值完全等于https://www.example.com的<a>元素。
3、开始匹配选择器:选择属性值以指定值开始的元素。使用=运算符。例如,[href="https://"] 会选择所有href属性值以https://开始的元素。
4、结束匹配选择器:选择属性值以指定值结束的元素。使用=".pdf"] 会选择所有href属性值以.pdf结束的元素。
5、包含匹配选择器:选择属性值包含指定字符串的元素。使用=运算符。例如,[title="hello"] 会选择所有title属性值包含hello字符串的元素。
6、词匹配选择器:选择属性值中包含指定单词的元素,单词由空格分隔。使用=运算符。例如,[class="active"] 会选择所有class属性值中包含单词active的元素。
7、前缀匹配选择器:选择属性值以指定值开始或者完全等于指定值的元素,主要用于语言代码或者文档类型这样的属性。使用|=运算符。例如,[lang|="en"] 会选择所有lang属性值以en开始或以en为值的元素。
五十五、事件冒泡
事件捕获和事件冒泡
如上图所示,事件流的触发顺序是:
- 事件捕获阶段,为截获事件提供了机会
- 实际的⽬标元素接收到事件
- 事件冒泡阶段,可在这个阶段对事件做出响应
addEventListener 方法如果不传第三个参数,默认是fasle,即冒泡操作。如果传true,则是捕获操作。
div.addEventListener('click ',function(ev) {
console.log( '捕获:2.div'); // 2
},true);
阻止事件冒泡:使用e.stopPropagation()、e.preventDefault()
开启事件捕获:一般默认是事件冒泡,可以通过在addEventListener方法的第三个参数中指定true来启用事件捕获。
事件委托
事件委托,就是利用了事件冒泡的机制,在较上层位置的元素上添加一个事件监听函数,来管理该元素及其所有子孙元素上的某一类的所有事件。
适用场景:在绑定大量事件的时候,可以选择事件委托
<ul id="list">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
<li>555</li>
</ul>
<script type="text/javascript">
// ⽗元素
var list = document.getElementById('list');
// 为⽗元素绑定事件,委托管理它的所有⼦元素li的点击事件
list.onclick = function (event) {
var currentTarget = event.target;
if (currentTarget.tagName.toLowerCase() === 'li') {
alert(currentTarget.innerText)
}
}
</script>
五十六、Picture 标签
浏览器的工作流程如下:
- 浏览器会先根据当前的情况,去匹配和使用
<source>
提供的图片 - 如果未匹配到合适的
<source>
,就使用<img>
标签提供的图片
<picture>
<source srcset="640.png" media="(min-width: 640px)">
<source srcset="480.png" media="(min-width: 480px)">
<img src="320.png" alt="">
</picture>
五十七、script 标签 defer 和 async
defer 和 async 的使用, 可以用于提升网页性能
script标签存在两个属性,defer和async,因此 script标签 的使用分为三种情况:
-
<script src="example.js"></script>
没有defer或async属性,浏览器会立即加载并执行相应的脚本。
不等待后续加载的文档元素,读到就开始加载和执行,此举会阻塞后续文档的加载 -
<script async src="example.js"></script>
有了async属性,表示后续文档的加载和渲染与js脚本的加载和执行是并行进行的,即异步执行; -
<script defer src="example.js"></script>
有了defer属性,加载后续文档的过程和js脚本的加载是并行进行的(异步),此时的js脚本仅加载不执行, js脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded事件触发执行之前。
defer和async对比:
- 1.defer和async在网络加载过程是一致的,都是异步执行的;(放在页面顶部, 也不会阻塞页面的加载, 与页面加载同时进行)
- 2.两者的区别, 脚本加载完成之后, async是立刻执行, defer会等一等 (等前面的defer脚本执行, 等dom的加载)
所以, js脚本加上 async 或 defer, 放在头部可以减少网页的下载加载时间, 如果不考虑兼容性, 可以用于优化页面加载的性能。
五十八、元素水平居中]()
1、margin: 0 auto;
2、转成行内块, 给父盒子设置 text-align: center
<div class="father">
<div class="center">我是内容盒子</div>
</div>
.father {
text-align: center;
}
.center {
width: 400px;
height: 400px;
background-color: pink;
display: inline-block;
}
3、flex布局
4、绝对布局+transform
<div class="father">
<div class="center">我是内容盒子</div>
</div>
.father {
background-color: skyblue;
position: relative;
height: 500px;
}
.center {
width: 400px;
height: 400px;
background-color: pink;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
五十九、CSS 中有哪些定位方式
static 正常文档流定位
- 此时设置 top、right、bottom、left 以及 z-index 都无效
- 块级元素遵循从上往下纵向排列,行级元素遵循从左到右排列
relative 相对定位
这个 “相对” 是指相对于正常文档流的位置。
absolute 绝对定位
- 当前元素相对于 **最近的非 static 定位的祖先元素 **来确定自己的偏移位置。
- 例如,当前为 absolute 的元素的父元素、祖父元素都为 relative,则当前元素会相对于父元素进行偏移定位。
fixed 固定定位
当前元素相对于屏幕视口 viewport 来确定自己的位置。并且当屏幕滚动时,当前元素的位置也不会发生改变。
sticky 粘性定位
这个定位方式有点像 relative 和 fixed 的结合。当它的父元素在视口区域、并进入 top 值给定的范围内时,当前元素就以 fixed 的方式进行定位,否则就以 relative 的方式进行定位。
<style>
* {
margin: 0;
padding: 0;
}
.header {
width: 100%;
height: 100px;
background-color: orange;
}
.nav {
width: 100%;
height: 200px;
background-color: pink;
position: sticky;
top: 0px;
}
.main {
width: 100%;
height: 100px;
background-color: skyblue;
}
</style>
<div class="header">我是头部</div>
<div class="nav">我是导航</div>
<div class="container">
<div class="main">我是主体部分1</div>
<div class="main">我是主体部分2</div>
<div class="main">我是主体部分3</div>
<div class="main">我是主体部分4</div>
<div class="main">我是主体部分5</div>
<div class="main">我是主体部分6</div>
<div class="main">我是主体部分7</div>
<div class="main">我是主体部分8</div>
</div>
六十、清除浮动
清除浮动的影响(子元素左右浮动,父元素高度无法撑起,解决方案有以下四种)
- 定高法: 给父元素强制设置高度
- 使用一个空的div,并设置样式
<div style="clear:both"></div>
- 为父元素添加
overflow: hidden
, 这样会触发BFC,形成独立渲染区域 - 定义一个 clearfix 样式类
.clearfix:after {
content: ""; /*设置内容为空*/
height: 0; /*高度为0*/
line-height: 0; /*行高为0*/
display: block; /*将文本转为块级元素*/
visibility: hidden; /*将元素隐藏*/
clear: both; /*清除浮动*/
}
.clearfix {
zoom: 1; /*为了兼容IE*/
}
说明:当前 flex 已成为主流布局方式,适应性强, 且稳定, 所以浮动使用率目前已逐步降低。
六十一、BFC
BFC :全称是 Block Formatting Context,块级格式化上下文。这是一个用于在盒模型下布局块级盒子的独立渲染区域,将处于BFC区域内和区域外的元素进行互相隔离。
满足下列条件之一就可触发BFC:
- HTML根元素
- position 值为
absolute
或fixed
- float 值不为
none
- overflow 值不为
visible
- display 值为
inline-block
、table-cell
或table-caption
BFC 的应用场景:
1. 场景一:防止两个相邻块级元素的上下 margin 发生重叠 (上下margin合并问题)
以下示例代码中的两个盒子的上下外边距会重合(即它们都设置了10px的外边距,我们期望它们之间的间距是 20px,但实际效果却只有 10px)。通过设置 display 为 inline-block 可以触发 BFC,可以解决此问题
<style>
.box1 {
width: 200px;
height: 100px;
background-color: red;
margin-bottom: 10px; /* 下外边距为 10px */
}
.box2 {
width: 200px;
height: 100px;
background-color: green;
margin-top: 10px; /* 上外边距为 10px */
// display: inline-block; /* 通过设置 display 为 inline-block 可以触发 BFC */
}
</style>
<div class="box1"></div>
<div class="box2"></div>
2、场景二:清除浮动
以下示例代码中, 容器元素 box1 的高度会没有高,而通过为 box1 添加 overflow: hidden , 触发BFC条件,可以让它的高度变回正常状态
<style>
.box1 {
width: 200px;
background-color: red;
//overflow: hidden;
}
.box2 {
float: left;
background-color: green;
}
</style>
<div class="box1">
<div class="box2">Hello,world</div>
<div class="box2">Hello,world</div>
<div class="box2">Hello,world</div>
</div>
场景三:实现自适应布局, 防止元素被浮动元素覆盖(左边固定, 右边自适应)
以下示例中,box2 会被设置了浮动的 box1 覆盖。要避免这种覆盖行为,可以让 box2 触发 BFC(overflow: hidden), 实现布局效果, 左边固定右边自适应
<style>
.box1 {
float: left;
width: 300px;
background-color: red;
height: 400px;
}
.box2 {
background-color: blue;
height: 600px;
//overflow: hidden; /* 将 overflow 设置为非 visible 值可触发 BFC */
}
</style>
<div class="box1"></div>
<div class="box2"></div>
六十二、箭头函数
- 以前函数中的this指向是根据如何调用来确定的。简单理解就是this指向调用者
- 箭头函数本身没有this,它只会沿用上一层作用域的this
- 并且由于箭头函数没有属于⾃⼰的
this
,它是不能被new
调⽤的。
<body>
<button class="btn1">点击</button>
<button class="btn2">5秒后启用</button>
<script>
// 1. 以前this的指向: 指向调用者
// console.log(this) // window
// // 普通函数
// function fn() {
// console.log(this) // window
// }
// window.fn()
// // 对象方法里面的this
// const obj = {
// name: 'andy',
// sayHi: function () {
// console.log(this) // obj
// }
// }
// obj.sayHi()
// 2. 箭头函数的中this指向-沿用上一层作用域的this
const fn = () => {
console.log(this) // window
}
fn()
// const obj = {
// name: 'andy',
// sayHi: () => {
// console.log(this) // window
// }
// }
// obj.sayHi()
const obj = {
name: 'andy',
sayHi: function () {
const fun = () => {
console.log(this) // obj
}
fun()
}
}
obj.sayHi()
// 3. 我们可以根据需求来选择是否使用箭头函数 this
// document.querySelector('.btn1').addEventListener('click', function () {
// this.style.color = 'red'
// })
document.querySelector('.btn1').addEventListener('click', () => {
// this.style.color = 'red'
// 此处不能用 this 指向 Window不是 按钮了
document.querySelector('.btn1').style.color = 'red'
})
document.querySelector('.btn2').addEventListener('click', function () {
this.disabled = true
// setTimeout(function () {
// console.log(this) // 定时器里面的this 指向 window
// this.disabled = false
// }, 5000)
setTimeout(() => {
console.log(this) //
this.disabled = false
}, 5000)
})
</script>
</body>
六十三、this指向
- 默认绑定:默认情况下,
this
会被绑定到全局对象上,比如在浏览器环境中就为window
对象,在node.js环境下为global
对象。 - 隐式绑定:如果函数的调用是从对象上发起时,则该函数中的
this
会被自动隐式绑定为对象。
function test() {
console.log(this.message);
}
let obj = {
message: "hello,world",
test: test
}
obj.test() // "hello,world"
- 显式绑定: 又叫做硬绑定,上下文调用模式, 想让this指向谁, this就指向谁。(硬绑定 => call apply bind)
function test() {
console.log(this.message);
}
let obj1 = {
message: "你好世界123"
}
let obj2 = {
message: "你好世界456"
}
test.bind(obj1)() // "你好世界123"
test.bind(obj2)() // "你好世界456"
- 构造函数:执行实例对象
- 箭头函数:本身不具有this,捕获上层this
六十四、call、bind、apply
call、bind、apply在JavaScript中都是用于改变函数执行时this指向的方法
call
-
call
方法能够在调用函数的同时指定this
的值 - 使用
call
方法调用函数时,第1个参数为this
指定的值 -
call
方法的其余参数会依次自动传入函数做为函数的参数
4、可以用于检测数据类型
<body>
<script>
// 1. 改变this指向 - call
const obj = { name: '佩奇' }
// call() 作用: 第一个调用函数 第二改变this指向
function fun(x, y) {
console.log(this)
// console.log(x + y)
return x + y
}
fun() // this 指向window
// fun.call(obj) // this 指向 obj 对象
// fun.call(obj, 1, 2) // this 指向 obj 对象
console.log(fun.call(obj, 1, 2)) // 返回值就是函数 返回值
// 2. call的应用场景 - 检测数据类型
// 2.1 typeof 检测数据类型不够精确的
console.log(typeof '123') // string
console.log(typeof []) // object
console.log(typeof null) // object
// 2.2 Object.prototype.toString() 返回的结果是[object xxx类型]
// console.log(Object.prototype.toString('123')) // [object Object]
console.log(Object.prototype.toString.call('123')) // [object String]
console.log(Object.prototype.toString.call(123)) // [object Number]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(null)) // [object Null]
</script>
</body>
apply
-
apply
方法能够在调用函数的同时指定this
的值 - 使用
apply
方法调用函数时,第1个参数为this
指定的值 -
apply
方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
// 改变this指向apply
// 1. 基本使用
const obj = { name: '佩奇' }
function fun(x, y) {
console.log(this)
console.log(x + y)
}
fun()
// fun.apply() // 1. 作用1调用函数
// fun.apply(obj) // 2. 作用2 改变this指向 obj
fun.apply(obj, [1, 2]) // 参数必须是数组
// 2. 使用场景- 求数组的最大值/最小值
console.log(Math.max(...[1, 2, 3])) // 3
// apply 或者 call 如果不需要改变this指向 写 null
console.log(Math.max.apply(null, [8, 2, 3])) // 8
console.log(Math.min.apply(null, [8, 2, 3])) // 2
bind
bind
方法并不会调用函数,而是创建一个指定了 this
值的新函数
// 使用场景 - 不需要调用函数,但是又想改变函数内部的this指向
// 1. 发送短信5秒倒计时业务
const codeBtn = document.querySelector('.code')
let flag = true // 开关变量,用来防止多次点击
codeBtn.addEventListener('click', function () {
if (flag) {
// 1.2 利用定时器做倒计时效果 setInterval
let i = 5
// 点击之后立马变化文字
this.innerHTML = `05秒后重新获取`
// 定时器
let timerId = setInterval(function () {
i--
this.innerHTML = `0${i}秒后重新获取`
// 1.3 时间到了 就显示文字为 重新获取
if (i === 0) {
this.innerHTML = `重新获取`
// 停止定时器
clearInterval(timerId)
flag = true
}
}.bind(this), 1000)//改变this指向,使其指向codeBtn,内部当作codeBtn使用
// 关闭开关
flag = false
}
})
总结
方法 | 相同点 | 传递参数 | 是否调用函数 | 使用场景 |
---|---|---|---|---|
call | 改变this指向 | 传递参数列表 arg1, arg2... | 调用函数 | Object.prototype.toString.call() 检测数据类型 |
apply | 改变this指向 | 参数是数组 | 调用函数 | 跟数组相关,比如求数组最大值和最小值等 |
bind | 改变this指向 | 传递参数列表 arg1, arg2... | 不调用函数 | 改变定时器内部的this指向 |
六十五、js 数组方法
- push(): 向数组的末尾添加一个或多个元素,并返回新的长度。
- pop(): 移除数组的最后一个元素,并返回被移除的元素。
- shift(): 移除数组的第一个元素,并返回被移除的元素。
- unshift(): 向数组的开头添加一个或多个元素,并返回新的长度。
- slice(): 返回一个新数组,包含从原数组中开始到结束(不包括结束)选择的元素。
- slice 是片的意思
let arr = [1, 2, 3, 4];
let sub = arr.slice(1, 3); // sub = [2, 3]
- splice() 方法是 JavaScript 数组中非常强大的一个方法,它可以用来添加、删除或替换数组中的元素。splice() 可以修改原数组,返回值是被删除的元素组成的数组。splice 中文是拼接的意思。
//start: 数组中开始修改的位置(索引)。如果超出了数组的边界,它会被设置为数组的长度,即不进行任何修改。
//deleteCount: 要删除的元素数量。如果这个参数是 0,则不会删除元素,如果超出了从 start 开始到数组末尾的元素数量,则会删除从 start 开始的所有元素。
//item1, item2, ...: 要添加到数组中的元素。
array.splice(start, deleteCount, item1, item2, ...);
//删除
let arr = [1, 2, 3, 4];
arr.splice(2, 1); // 删除索引2处的元素,arr = [1, 2, 4]
let arr = [1, 2, 3, 4];
arr.splice(1); // 删除索引1到末尾的所有元素,arr = [1]
//增加
let arr = [1, 2, 4];
arr.splice(2, 0, 3); // 在索引2处添加元素3,arr = [1, 2, 3, 4]
//替换
let arr = [1, 2, 3, 4];
arr.splice(1, 1, 'a'); // 删除索引1处的元素,添加'a',arr = [1, 'a', 3, 4]
//组合
let arr = [1, 2, 3, 4];
arr.splice(1, 2, 'a', 'b'); // 删除索引1和2处的元素,添加'a'和'b',arr = [1, 'a', 'b', 4]
//注意事项:
//1、如果 start 索引超出了数组的当前长度,splice() 会将超出的部分视为添加操作。