vue 事件总线EventBus的概念、使用以及注意点
前言
vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 EventBus的概念
正文
EventBus的简介
EventBus的使用
一、初始化
import Vue from 'vue'
//因为是全局的一个'仓库',所以初始化要在全局初始化
const EventBus = new Vue()
//在已经创建好的Vue实例原型中创建一个EventBus
Vue.prototype.$EventBus = new Vue()
二、向EventBus发送事件
发送事件的语法:this.$EventBus.$emit(发送的事件名,传递的参数)
已经创建好EventBus后我们就需要向它发送需要传递的事件,以便其他组件可以向EventBus获取。
例子:有两个组件A和B需要通信,他们不是父子组件关系,B事件需要获得A事件里的一组数据data
<!-- A.vue 这里是以模块化的方式讲解的,即A组件和B组件分别各自
一个.vue文件,所以代码中会有导入的语法-->
<template>
<button @click="sendMsg">发送MsgA</button>
</template>
<script>
export default {
data(){
return{
MsgA: 'A组件中的Msg'
}
},
methods: {
sendMsg() {
/*调用全局Vue实例中的$EventBus事件总线中的$emit属性,发送事
件"aMsg",并携带A组件中的Msg*/
this.$EventBus.$emit("aMsg", this.MsgA);
}
}
};
</script>
三、接收事件
<!-- B.vue -->
<template>
<!-- 展示msgB -->
<p>{{msgB}}</p>
</template>
<script>
export default {
data(){
return {
//初始化一个msgB
msgB: ''
}
},
mounted() {
/*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
到事件总线中的aMsg事件*/
this.$EventBus.$on("aMsg", (data) => {
//将A组件传递过来的参数data赋值给msgB
this.msgB = data;
});
}
};
</script>
这样,B组件就轻松接收到了A组件传递过来的参数,并成功展示了该参数,这样是不是就很简单的解决了各组件之间的通讯呢?虽然EventBus是一个很轻便的方法,任何数据都可以往里传,然后被别的组件获取,但是如果用不好,容易出现很严重的BUG,所以接下来我们就来讲解一下移除监听事件。
四、移除监听事件
在上一个例子中,我们A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听了,如果反复进入B组件多次,那么就会对aMsg进行多次的监听。
总而言之,A组件只向EventBus发送了一次事件,但B组件却进行了多次监听,EventBus容器中有很多个一模一样的事件监听器这时就会出现,事件只触发一次,但监听事件中的回调函数执行了很多次
<!-- B.vue -->
<template>
<!-- 展示msgB -->
<p>{{msgB}}</p>
</template>
<script>
export default {
data(){
return {
//初始化一个msgB
msgB: ''
}
},
mounted() {
/*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
到事件总线中的aMsg事件*/
this.$EventBus.$on("aMsg", (data) => {
//将A组件传递过来的参数data赋值给msgB
this.msgB = data;
});
},
beforeDestroy(){
//移除监听事件"aMsg"
this.$EventBus.$off("aMsg")
}
};
</script>
结束语
无废话快速上手React路由
安装
// npm
npm install react-router-dom
// yarn
yarn add react-router-dom
react-router相关标签
import {
BrowserRouter,
HashRouter,
Route,
Redirect,
Switch,
Link,
NavLink,
withRouter,
} from 'react-router-dom'
简单路由跳转
import {
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
<Link to="/home" className="link">跳转Home页面</Link>
<Link to="/about" className="link">跳转About页面</Link>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Router>
</div>
);
}
export default App;
嵌套路由跳转
import {
BrowserRouter as Router,
Route,
Link,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
<Link to="/home">跳转Home页面</Link>
<Link to="/about">跳转About页面</Link>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Router>
</div>
);
}
export default App;
然后 Home
组件中同样也想设置两个路由组件的匹配路径,分别是 /home/one
和 /home/two
,此时就可以看出,这个 /home/one
和 /home/two
为上一级路由 /home
的二级嵌套路由,代码如下:
import React from 'react'
import {
Route,
Link,
} from 'react-router-dom'
import One from './one'
import Two from './two'
function Home () {
return (
<>
我是Home页面
<Link to="/home/one">跳转到Home/one页面</Link>
<Link to="/home/two">跳转到Home/two页面</Link>
<Route path="/home/one" component={One}/>
<Route path="/home/two" component={Two}/>
</>
)
}
export default Home
特别注意: Home
组件中的路由组件 One
的二级路由路径匹配必须要写 /home/one
,而不是 /one
,不要以为 One
组件看似在 Home
组件内就可以简写成 /one
动态链接
import {
BrowserRouter as Router,
Route,
NavLink
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Router>
</div>
);
}
export default App;
/* 设置active类的样式 */
.active {
font-weight: blod;
color: red;
}
路由匹配优化
import {
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Home}/>
{/* 此处省略一万个Route组件 */}
<Route path="/home" component={Home}/>
</Switch>
</Router>
</div>
);
}
export default App;
重定向
import {
BrowserRouter as Router,
Route,
NavLink,
Switch,
Redirect,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<NavLink to="/shop" className="link">跳转Shop页面</NavLink> {/* 点击,跳转到/shop,但该路径没有设置 */}
<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Redirect to="/home" /> {/* 当以上Route组件都匹配失败时,重定向到/home */}
</Switch>
</Router>
</div>
);
}
export default App;
路由传参
第一种
import {
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
{/* 在 /home 的路径上携带了 张三、18 共两个参数 */}
<NavLink to="/home/张三/18" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Switch>
{/* 在 /home 匹配路径上相同的位置接收了 name、age 两个参数 */}
<Route path="/home/:name/:age" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Router>
</div>
);
}
export default App;
第二种
import {
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
{/* 在跳转路径后面以?开头传递两个参数,分别为name=张三、age=18 */}
<NavLink to="/home?name=张三&age=18" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Switch>
{/* 此处无需做接收操作 */}
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Router>
</div>
);
}
export default App;
第三种
import {
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'
function App() {
return (
<div className="App">
<Router>
{/* 以对象的形式描述to属性,路径属性名为pathname,参数属性名为state */}
<NavLink to={{pathname: "/home", state: {name: '张三', age: 18}}} className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Switch>
{/* 此处无需特地接收属性 */}
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Router>
</div>
);
}
export default App;
函数式路由
push
import React from 'react'
function Home (props) {
let pushLink = () => {
props.history.push('/about')
}
return (
<div className="a">
我是Home页面
<button onClick={pushLink}>跳转到about页面</button>
</div>
)
}
export default Home
replace
import React from 'react'
function Home (props) {
let replaceLink = () => {
props.history.replace('/about')
}
return (
<div className="a">
我是Home页面
<button onClick={replaceLink}>跳转到about页面</button>
</div>
)
}
export default Home
goForward
goBack
go
收起阅读 »vue数据可视化界面,智慧图表。Echarts,以及git
一、数据图表
1.1HighChart
兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库
下载
一、通过CDN
https://code.highcharts.com.cn/index.html
二、通过NPM下载(用的比较多)
npm install highcharts
三、通过官网下载
https://www.highcharts.com.cn/download
通过引入库的方式引入到本地
基本应用
Document
1.2Echarts(用的更多一些)
一、通过CDN
jsdelivr.com/package/npm/echarts
二、通过NPM(通过NPM)
npm install echarts
三、通过官网
https://echarts.apache.org/zh/download.html
四、通过github
https://github.com/apache/echarts/releases
1.3如何在vue脚手架中引入Echarts
//全局引入echart
import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts
home.vue
二、git代码管理
2.1代码管理工具
svn (小乌龟)
https://tortoisesvn.net/
git (命令)
github(所有开源项目的归属地)
https://github.com/
码云
https://gitee.com/
git软件
https://git-scm.com/
无论是gihub还是码云,他们都是用git命令去操作的。所以命令都一样
git软件的安装,下一步,下一步,傻瓜式安装即可
装成功的状态: 鼠标右键看到 git Bash Here 就OK
2.2创建一个远程仓库 (新项目)
一、登录github/码云输入用户名密码
二、新建一个远程仓库,在官网右上角(点击+ )
三、创建一个仓库名称,添加仓库描述,创建一个公有的仓库,不需要为仓库创建其他内容
一、在本地创建一个文件夹,创建相关的基本骨架
二、初始化当前文件夹变成本地仓库(会出现一个.git的隐藏文件)
git init
三、本地的所有内容上传到暂缓区
git add .
四、提交的时候要做记录
git commit -m '尽量写英文,非要写写中文也可以'
五、链接远程仓库
git remote add origin https://gitee.com/zhangzhangzhangdada/shop-admin.git
六、把暂缓区的内容推送到远程仓库 (master 默认的分支名字)
git push -u origin master
原文:https://blog.csdn.net/weixin_49030317/article/details/116666179
收起阅读 »JavaScript解密之旅-----数组的遍历方法总结
数组的循环
一、forEach()
二、map()
三、filter()
四、reduce()与reduceRight()
五、every()
六、some()
七、find()与findIndex()
八、 for in
九、 for of
十、 for
总结
数组的循环
一、forEach()
对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。
参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身。
var arr = [1, 2, 3, 4, 5];
arr.forEach(function (item, index, arr) {
console.log(item, index, arr);
// item:遍历的数组内容,index:第对应的数组索引,arr:数组本身。
});
二、map()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.map(function (item) {
return item * item;
});
console.log(arr2); //[1, 4, 9, 16, 25]
三、filter()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.filter(function (x, index) {
return x % 2 == 0 || index >= 2;
});
console.log(arr2); // [2,3,4,5]
四、reduce()与reduceRight()
// reduce()
let array = [1, 2, 3, 4, 5];
let arrayNew = array.reduce((x, y) => {
console.log("x===>" + x);
console.log("y===>" + y);
console.log("x+y===>", Number(x) + Number(y));
return x + y;
});
console.log("arrayNew", arrayNew); // 15
console.log(array); // [1, 2, 3, 4, 5]
// reduceRight() 只是执行数组顺序为倒序
五、every()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.every(function (x) {
return x < 8;
});
console.log(arr2); //true
var arr3 = arr.every(function (x) {
return x < 3;
});
console.log(arr3); // false
六、some()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.some(function(x) {
return x < 3;
});
console.log(arr2); //true
var arr3 = arr.some(function(x) {
return x > 6;
});
console.log(arr3); // false
七、find()与findIndex()
// find()
let arr = [1, 2, 3, 4, 5];
let res = arr.find(function (val, index, arr) {
return val > 3;
});
console.log(res); //4
// findIndex
let arr = [1, 2, 3, 4, 5];
let res = arr.findIndex(function (val, index, arr) {
return val > 3;
});
console.log(res); //3
八、 for in
var arr = [
{ id: 1, name: "程序员" },
{ id: 2, name: "掉头发" },
{ id: 3, name: "你不信" },
{ id: 4, name: "薅一下" },
];
var arrNew = [];
for (var key in arr) {
console.log(key);
console.log(arr[key]);
arrNew.push(arr[key].id);
}
console.log(arrNew);
九、 for of
var arr = [
{ name: "程序员" },
{ name: "掉头发" },
{ name: "你不信" },
{ name: "薅一下" },
];
// key()是对键名的遍历;
// value()是对键值的遍历;
// entries()是对键值对的遍历;
for (let item of arr) {
console.log(item);
}
// 输出数组索引
for (let item of arr.keys()) {
console.log(item);
}
// 输出内容和索引
for (let [item, val] of arr.entries()) {
console.table(item + ":" + val);
}
十、 for
var arr = [
{ name: "程序员" },
{ name: "掉头发" },
{ name: "你不信" },
{ name: "薅一下" },
];
for (let index = 0; index < arr.length; index++) {
const element = arr[index];
console.log(element )
}
总结
7种经常使用的Vue.js模式?你居然还不知道!!
说实话,阅读文档并不是我们大多数人喜欢的事情,但当使用像Vue这样不断发展的现代前端框架时,很多东西会随着每一个新版本的发布而改变,你可能会错过一些后来推出的新的闪亮功能。让我们看一下那些有趣但不那么流行的功能和优化的写法。请记住,所有这些都是Vue文档的一部分。
7种Vue.js模式
1.处理加载状态
在大型应用程序中,我们可能需要将应用程序划分为更小的块,只有在需要时才从服务器加载组件。为了使这一点更容易,Vue允许你将你的组件定义为一个工厂函数,它异步解析你的组件定义。Vue只有在需要渲染组件时才会触发工厂函数,并将缓存结果,以便将来重新渲染。2.3版本的新功能是,异步组件工厂也可以返回一个如下格式的对象。
const AsyncComponent = () => ({
// 要加载的组件(应为Promise)
component: import('./MyComponent.vue'),
// 异步组件正在加载时要使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 显示加载组件之前的延迟。默认值:200ms。
delay: 200,
// 如果提供并超过了超时,则会显示error组件。默认值:无穷。
timeout: 3000
})
通过这种方法,你有额外的加载和错误状态、组件获取的延迟和超时等选项。
2.廉价的“v-once”静态组件
在Vue中渲染纯HTML元素的速度非常快,但有时你可能有一个包含大量静态内容的组件。在这种情况下,你可以通过在根元素中添加 v-once 指令来确保它只被评估一次,然后进行缓存,就像这样。
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
3.递归组件
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
像上面这样的组件会导致“超过最大堆栈大小”的错误,所以要确保递归调用是有条件的即(使用 v-if 最终将为 false)
4.内联模板
当特殊属性 inline-template 存在于一个子组件上时,该组件将使用它的内部内容作为它的模板,而不是将其视为分布式内容,这允许更灵活的模板编写。
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>
5.动态指令参数
指令参数可以是动态的。例如,在 v-mydirective:[argument]=“value" 中, argument 可以根据组件实例中的数据属性更新!这使得我们的自定义指令可以灵活地在整个应用程序中使用。
这是一条指令,其中可以根据组件实例更新动态参数:
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
var s = (binding.arg == 'left' ? 'left' : 'top')
el.style[s] = binding.value + 'px'
}
})
new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left'
}
}
})
6.事件和键修饰符
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
7.依赖注入(Provide/Inject)
有几种方法可以让两个组件在 Vue 中进行通信,它们各有优缺点。在2.2版本中引入的一种新方法是使用Provide/Inject的依赖注入。
这对选项一起使用,允许一个祖先组件作为其所有子孙的依赖注入器,无论组件层次结构有多深,只要它们在同一个父链上。如果你熟悉React,这与React的上下文功(context)能非常相似。
// parent component providing 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// child component injecting 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
vue传值方式总结 (十二种方法)
一.父传子传递
//父组件
<template>
<div>
<i>父组件</i>
<!--页面使用-->
<son :data='name'></son>
</div>
</template>
<script>
import son from "./son.vue";//导入父组件
export default {
components: { son },//注册组件
name: "父组件",
data() {
return {
name: "Frazier", //父组件定义变量
};
},
};
</script>
//子组件
<template>
<div>{{data}}</div>
</template>
<script>
export default {
components: { },
name: '子组件',
props:["data"],
};
</script>
二.子传父传递
//父组件
<template>
<div>
<i>父组件</i>
<!--页面使用-->
<son @lcclick="lcclick"></son>//自定义一个事件
</div>
</template>
<script>
import son from "./son.vue"; //导入父组件
export default {
components: { son }, //注册组件
name: "父组件",
data() {
return {};
},
methods: {
lcclick(){
alert('子传父')
}
},
};
</script>
//子组件
<template>
<div>
<button @click="lcalter">点我</button>
</div>
</template>
<script>
export default {
components: { },
name: '子组件',
methods: {
lcalter(){
this.$emit('lcclick')//通过emit来触发事件
}
},
};
</script>
三.兄弟组件通信(bus总线)
(1)在src中新建一个Bus.js的文件,然后导出一个空的vue实例
(2)在传输数据的一方引入Bus.js 然后通过Bus.e m i t ( “ 事 件 名 ” , " 参 数 " ) 来 来 派 发 事 件 , 数 据 是 以 emit(“事件名”,"参数")来来派发事件,数据是以emit(“事件名”,"参数")来来派发事件,数据是以emit()的参 数形式来传递
(3)在接受的数据的一方 引入 Bus.js 然后通过 Bus.$on(“事件名”,(data)=>{data是接受的数据})
图片示例:
四.ref/refs(父子组件通信)
//父组件
<template>
<div>
<button @click="sayHello">sayHello</button>
<child ref="childForRef"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data () {
return {
childForRef: null,
}
},
mounted() {
this.childForRef = this.$refs.childForRef;
console.log(this.childForRef.name);
},
methods: {
sayHello() {
this.childForRef.sayHello()
}
}
}
</script>
//子组件
<template>
<div>child 的内容</div>
</template>
<script>
export default {
data () {
return {
name: '我是 child',
}
},
methods: {
sayHello () {
console.log('hello');
alert('hello');
}
}
}
</script>
五.Vuex通信
//父组件
template>
<div id="app">
<ChildA/>
<ChildB/>
</div>
</template>
<script>
import ChildA from './ChildA' // 导入A组件
import ChildB from './ChildB' // 导入B组件
export default {
components: {ChildA, ChildB} // 注册组件
}
</script>
//子组件A
<template>
<div id="childA">
<h1>我是A组件</h1>
<button @click="transform">点我让B组件接收到数据</button>
<p>因为点了B,所以信息发生了变化:{{BMessage}}</p>
</div>
</template>
<script>
export default {
data() {
return {
AMessage: 'Hello,B组件,我是A组件'
}
},
computed: {
BMessage() {
// 这里存储从store里获取的B组件的数据
return this.$store.state.BMsg
}
},
methods: {
transform() {
// 触发receiveAMsg,将A组件的数据存放到store里去
this.$store.commit('receiveAMsg', {
AMsg: this.AMessage
})
}
}
}
</script>
//子组件B
<template>
<div id="childB">
<h1>我是B组件</h1>
<button @click="transform">点我让A组件接收到数据</button>
<p>点了A,我的信息发生了变化:{{AMessage}}</p>
</div>
</template>
<script>
export default {
data() {
return {
BMessage: 'Hello,A组件,我是B组件'
}
},
computed: {
AMessage() {
// 这里存储从store里获取的A组件的数据
return this.$store.state.AMsg
}
},
methods: {
transform() {
// 触发receiveBMsg,将B组件的数据存放到store里去
this.$store.commit('receiveBMsg', {
BMsg: this.BMessage
})
}
}
}
</script>
//vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
AMsg: '',
BMsg: ''
}
const mutations = {
receiveAMsg(state, payload) {
// 将A组件的数据存放于state
state.AMsg = payload.AMsg
},
receiveBMsg(state, payload) {
// 将B组件的数据存放于state
state.BMsg = payload.BMsg
}
}
export default new Vuex.Store({
state,
mutations
})
六.$parent
通过parent可以获父组件实例 ,然 后通过这个实例就可以访问父组件的属 性和方法 ,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟root,可以获取根组件实例。
代码示例:
// 获父组件的数据
this.$parent.foo
// 写入父组件的数据
this.$parent.foo = 2
// 访问父组件的计算属性
this.$parent.bar
// 调用父组件的方法
this.$parent.baz()
//在子组件传给父组件例子中,可以使用this.$parent.getNum(100)传值给父组件。
七.sessionStorage传值
// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');
// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');
// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');
// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();
注意:里面存的是键值对,只能是字符串类型,如果要存对象的话,需要使用 let objStr = JSON.stringify(obj) 转成字符串然后再存储(使用的时候 let obj = JSON.parse(objStr) 解析为对象)。
推荐一个库 good-storage ,它封装了sessionStorage ,可以直接用它的API存对象
//localStorage
storage.set(key,val)
storage.get(key, def)
//sessionStorage
storage.session.set(key, val)
storage.session.get(key, val)
八.路由传值
使用问号传值
A页面跳转B页面时使用 this.r o u t e r . p u s h ( ’ / B ? n a m e = d a n s e e k ’ ) B 页 面 可 以 使 用 t h i s . router.push(’/B?name=danseek’) B页面可以使用 this.router.push(’/B?name=danseek’)B页面可以使用this.route.query.name 来获取A页面传过来的值
上面要注意router和route的区别
使用冒号传值
配置如下路由:
{
path: '/b/:name',
name: 'b',
component: () => import( '../views/B.vue')
},
在B页面可以通过 this.$route.params.name 来获取路由传入的name的值
使用父子组件传值
由于router-view本身也是一个组件,所以我们也可以使用父子组件传值方式传值,然后在对应的子页面里加上props,因为type更新后没有刷新路由,所以不能直接在子页面的mounted钩子里直接获取最新type的值,而要使用watch
<router-view :type="type"></router-view>
// 子页面
props: ['type']
watch: {
type(){
// console.log("在这个方法可以时刻获取最新的数据:type=",this.type)
},
},
九.祖传孙 $attrs
<template>
<section>
<parent name="grandParent" sex="男" age="88" hobby="code" @sayKnow="sayKnow"></parent>
</section>
</template>
<script>
import Parent from './Parent'
export default {
name: "GrandParent",
components: {
Parent
},
data() {
return {}
},
methods: {
sayKnow(val){
console.log(val)
}
},
mounted() {
}
}
</script>
template>
<section>
<p>父组件收到</p>
<p>祖父的名字:{{name}}</p>
<children v-bind="$attrs" v-on="$listeners"></children>
</section>
</template>
<script>
import Children from './Children'
export default {
name: "Parent",
components: {
Children
},
// 父组件接收了name,所以name值是不会传到子组件的
props:['name'],
data() {
return {}
},
methods: {},
mounted() {
}
}
</script>
<template>
<section>
<p>子组件收到</p>
<p>祖父的名字:{{name}}</p>
<p>祖父的性别:{{sex}}</p>
<p>祖父的年龄:{{age}}</p>
<p>祖父的爱好:{{hobby}}</p>
<button @click="sayKnow">我知道啦</button>
</section>
</template>
<script>
export default {
name: "Children",
components: {},
// 由于父组件已经接收了name属性,所以name不会传到子组件了
props:['sex','age','hobby','name'],
data() {
return {}
},
methods: {
sayKnow(){
this.$emit('sayKnow','我知道啦')
}
},
mounted() {
}
}
</script>
十.孙传祖使用$listeners
<template>
<div id="app">
<children-one @eventOne="eventOne"></children-one>
{{ msg }}
</div>
</template>
<script>
import ChildrenOne from '../src/components/children.vue'
export default {
name: 'App',
components: {
ChildrenOne,
},
data() {
return {
msg: ''
}
},
methods: {
eventOne(value) {
this.msg = value
}
}
}
</script>
//父组件
<template>
<div>
<children-two v-on="$listeners"></children-two>
</div>
</template>
<script>
import ChildrenTwo from './childrenTwo.vue'
export default {
name: 'childrenOne',
components: {
ChildrenTwo
}
}
</script>
//子组建
<template>
<div>
<button @click="setMsg">点击传给祖父</button>
</div>
</template>
<script>
export default {
name: 'children',
methods: {
setMsg() {
this.$emit('eventOne', '123')
}
}
}
</script>
十一.promise传参
//类似与这样使用,但实际上后面两个参数无法获取
promise = new Promise((resolve,reject)=>{
let a = 1
let b = 2
let c = 3
resolve(a,b,c)
})
promise.then((a,b,c)=>{
console.log(a,b,c)
})
resolve() 只能接受并处理一个参数,多余的参数会被忽略掉。
如果想多个用数组,或者对象方式。。
数组
promise = new Promise((resolve,reject)=>{
resolve([1,2,3])
})
promise.then((arr)=>{
console.log(arr[0],arr[1],arr[2])
})
对象
promise = new Promise((resolve,reject)=>{
resolve({a:1,b:2,c:3})
})
promise.then(obj=>{
console.log(obj.a,obj.b,obj.c)
})
十二.全局变量
本文链接:https://blog.csdn.net/Frazier1995/article/details/116069811
前端必须要了解的一些知识 (十一)
六种基本数据类型
undefined
null
string
boolean
number
symbol(ES6)
- 一种引用类型
- Object
string
- length属性
- prototype 添加的方法或属性在所有的实例上共享
- charAt(index) 返回值
- charCodeAt(index) 返回字符的Unicode编码
- indexOf(searchVal,index) 值所在的位置 param2是从位置开始算
- search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符
var str = 'abcDEF';
console.log(str.search('c')); //返回2
console.log(str.search('d')); //返回-1
console.log(str.search(/d/i)); //返回3
Object
- 对象可以通过执行new操作符后跟要创建的对象类型的名称来创建。
前端错误的分类
即时运行错误:代码错误
资源加载错误
错误的捕获方式
代码错误
try...catch
window.onerror
资源错误
object.onerror(不会冒泡到window):节点上绑定error事件
performance.getEntries:获取资源的加载时长
error的事件捕获:用捕获不用冒泡可以监控
上报错误的基本原理
1.ajax通讯方式上报
2.image对象上报
跨域的代码错误怎么捕获
前端必须要了解的一些知识 (十)
console.log(1)
setTimeout(){
console.log(2)
}
console.log(3)
1,3,2
console.log(A)
while(true){
}
console.log(B)
//只输出A while是个同步队列 。 进入死循环
----------------------------
console.log(A)
settimeout(){
console.log(B)
}
while(true){
}
//仍然只输出A 。 同步没执行完不会执行异步
-----------------------------
for(var i=0;i<4;i++){
settimeout(()=>{
console.log(i)
},1000)
}
//4次4
前端必须要了解的一些知识 (九)
前端必须要了解的一些知识 (八)
前端必须要了解的一些知识 (七)
//第一种:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});
//第二种 通过构造函数
var M = function (name) { this.name = name; };
var o3 = new M('o3');
//第三种 Object.create
var p = {name: 'p'};
var o4 = Object.create(p);
1,构造函数
fn Animal(){
this.name = 'name'
}
2,ES6class的声明
class Animal2 {
//↓↓构造函数
constructor(){
this.name=name
}
}
实例化类
console.log(new Animal(),new Animal2())
fn parent1(){
this.name= 'parent1'
}
parent1.prototype.say(){
console.log('hello')
}
fn child1() {
parent1.call(this)
this.type='child1'
}
fn parent2(){
this.name= 'parent2'
this.play=[1,2,3]
}
fn child2() {
this.type='child2'
}
child2.prototype = new parent2()
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
fn parent3(){
this.name= 'parent3'
this.play=[1,2,3]
}
fn child3() {
parent3.call(this)
this.type='child2'
}
child3.prototype = new parent3()
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play,s4.play)
fn parent4(){
this.name= 'parent4'
this.play=[1,2,3]
}
fn child4() {
parent4.call(this)
this.type='child4'
}
child4.prototype = parent4.prototype
var s5 = new Child4();
var s6 = new Child4();
console.log(s5 instance child4,s5 instance parent4)//true true
console.log(s5.constructor)//parent4
缺点:区分不了s5是child4还是parent4的实例
//优化方式
fn parent5(){
this.name= 'parent5'
this.play=[1,2,3]
}
fn child5() {
parent5.call(this)
this.type='child5'
}
child5.prototype = Object.creat(parent5.prototype)
//此时还是找不到 创建一下constructor可解决
child5.prototype.constructor = child5
var s7 = new Child5();
console.log(s7 instance child4,s7 instance parent4)//true true
console.log(s7.constructor)//child5
前端必须要了解的一些知识 (六)
DOM事件的级别
DOM0
element.onclick=function(){}
DOM1
未制定事件相关的标准
DOM2
element.add('click',fn,false)/ie . atench
DOM3
el.add('keyup',fn,false)增加了其他事件除了click
DOM事件的模型:捕获和冒泡
DOM事件流
三个j阶段
捕获 。 目标阶段 。 冒泡阶段
事件捕获的具体流程
window=>document=>html=>body=>.....目标
冒泡则相反
event对象的常见应用
event.preventDefalut . 阻止默认行为
event.stopPropagation . 阻止冒泡
event.stoplmmediatePropagation . 事件响应优先级
事件代理
event.currentTarget 当前绑定的事件的对象
event.target 返回触发事件的元素
currentTarget在事件流的捕获,冒泡阶段。只有当事件流处在冒泡阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,
target指向被单击的对象
currentTarget指向当前事件活动的对象(一般为父级)。
自定义事件
let eve = new Event('eveName')/new CustomEvent可以加参数Obj
//注册
ev.addEventListener('eveName',fn)
//触发
ev.dispatchEvent(eve)
HTTP
http协议包括 :通用头域、请求消息、响应消息和主体信息。
特点
简单快速
每个资源得url是固定得
灵活
无连接
连接一次就会断掉
无状态
服务端不记录客户端连接得身份
报文得组成部分
请求报文
请求行
http方法
页面地址
http协议以及http版本
请求头
key value值告诉服务端我要哪些内容
空行
隔断
请求体
数据
响应报文
状态行
协议 状态吗
响应头
key value
空行
隔断
相应体
数据
http方法
get 获取资源
post 传输资源
put 更新资源
delete 删除资源
HEAD 获取报文首部
POST和GET区别(记住以下三个以上1,3,4,6,9)
HTTP状态码
持久链接
http1.1版本支持
管线化
- 管线化得特点和原理
- 请求和响应打包返回
- 持续连接完成后进行的且需要1.1版本的支持
- 管线化只有get和head可以进行 post有限制
- 管线化默认chrome和firefox默认不开启,初次连接的时候可能不支持,需要服务端的支持
前端必须要了解的一些知识 (五)
- 根元素
- float的值不为none
- overflow的值不为visible
- display的值为inline-block、table-cell、table-caption
- position的值为absolute或fixed
- 内部的Box会在垂直方向上一个接一个的放置
- 垂直方向上的距离由margin决定。(完整的说法是:属于同一个BFC的两个相邻Box的margin会发生重叠,与方向无关。)
- 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
- BFC的区域不会与float的元素区域重叠
- 计算BFC的高度时,浮动子元素也参与计算
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然
前端必须要了解的一些知识 (三)
你在下单时,要给后台发请求,后台通过拿到的参数请求微信后台去生成订单并同时返给你一个路径mweb_url,这个路径就是用来调起微信应用发起支付操作的。(这里要说明的就是由于h5支付不能主动回调,所以需要个主动查询的操作,这时会需要你又一个确认界面来进行主动查询订单状态。这里是个坑一会儿再说),调起支付界面之后进行支付操作,期间你什么都不用管,因为这都是微信的事。你需要的就是在你付完钱之后查看你的钱买你要的东西到底有没有成功(你要是不加的话,谁知道成功没,估计顾客会打死你,付完钱就茫然了,不知道到底钱到哪去了→_→)
普通函数中的this:
1. this总是代表它的直接调用者, 例如 obj.func ,那么func中的this就是obj
2.在默认情况(非严格模式下,未使用 'use strict'),没找到直接调用者,则this指的是 window
3.在严格模式下,没有直接调用者的函数中的this是 undefined
4.使用call,apply,bind(ES5新增)绑定的,this指的是 绑定的对象
箭头函数中的this
默认指向在定义它时,它所处的对象,而不是执行时的对象, 定义它的时候,可能环境是window(即继承父级的this);
下面通过一些例子来研究一下 this的一些使用场景
call
call(null, arr[0], arr[1], arr[2], arr[3], arr[4])//89
1 dom有元素 页面不渲染
首页 scoped 不加 导致引入的tab分类无法加载图片
原因未知 此处感觉不太球对 瞎吉儿改的
2:懒加载问题
3.vue router
repalce push go
4css使图片置灰
-webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); filter: gray;
前端必须要了解的一些知识 (二)
获取字符串长度 str.length
分割字符串 str.split()
拼接字符串 str1+str2 或 str1.concat(str2)
替换字符串 str.replace(“玩游戏”,”好好学习”)
提取子字符串 str.slice(start, end)或str.substring(start,end)或myStr.substr(start,length)
切换字符串大小写 str.toLowerCase()和str.toUpperCase()
比较字符串 str1.localeCompare(str2)
匹配字符串 str.match(pattern)或pattern.exec(str)或str.search(pattern)
根据位置查字符 str.charAt(index)
根据位置字符Unicode编码 str.charCodeAt(index)
根据字符查位置 str.indexOf(“you”)从左,myStr.lastIndexOf(“you”)从尾 或str.search(‘you’)
原始数据类型转字符串 String(数据) 或利用加号
字符串转原始数据类型 数字Number(”) // 0 布尔Boolean(”) // 0
自己构建属性和方法 String.prototype.属性或方法= function(参数){代码}
----------
箭头函数需要注意的地方
*当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。
1、在使用=>定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象;
2、不能够用作构造函数,这就是说,不能够使用new命令,否则就会抛出一个错误;
3、不能够使用arguments对象;
4、不能使用yield命令;
-------------------------
let和const
*let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。const定义常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。
1、let声明的变量具有块级作用域
2、let声明的变量不能通过window.变量名进行访问
3、形如for(let x..)的循环是每次迭代都为x创建新的绑定
依次输出的问题
1:立即执行函数
2:闭包
3:let
--------------------------------
Set数据结构
*es6方法,Set本身是一个构造函数,它类似于数组,但是成员值都是唯一的。
--------------------------------
-------------------------------------
promise 案例较多 。 建议看代码
http://www.cnblogs.com/fengxiongZz/p/8191503.html
收起阅读 »前端必须要了解的一些知识 (一)
常用api
moveTo(x0,y0):把当前画笔(ictx)移动到(x0,y0)这个位置。
lineTo(x1,y1):从当前位置(x0,y0)处到(x1,y1)画一条直线。
beginPath():开启一条路径或者重置当前路径。
closePath():从当前点回到路径起始点,也就是上一个beginPath的位置,回避和路径。
stroke():绘制。必须加了这个函数才会画图,所以这个一定要放在最后。
绘制一个圆形
/获取Canvas对象(画布)
var canvas = document.getElementById("myCanvas");
//简单地检测当前浏览器是否支持Canvas对象,以免在一些不支持html5的浏览器中提示语法错误
if(canvas.getContext){
//获取对应的CanvasRenderingContext2D对象(画笔)
var ctx = canvas.getContext("2d");
//开始一个新的绘制路径
ctx.beginPath();
//设置弧线的颜色为蓝色
ctx.strokeStyle = "blue";
var circle = {
x : 100, //圆心的x轴坐标值
y : 100, //圆心的y轴坐标值
r : 50 //圆的半径
};
//沿着坐标点(100,100)为圆心、半径为50px的圆的顺时针方向绘制弧线
ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI / 2, false);
//按照指定的路径绘制弧线
ctx.stroke();
}
------
深拷贝
深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个
1:不仅可拷贝数组还能拷贝对象(但不能拷贝函数)
var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}] var new_arr = JSON.parse(JSON.stringify(arr)) console.log(new_arr);
2:下面是深拷贝一个通用方法,实现思路:拷贝的时候判断属性值的类型,如果是对象,继续递归调用深拷贝函数
var deepCopy = function(obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是一个对象
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
// 遍历obj,并且判断是obj的属性才拷贝
if (obj.hasOwnProperty(key)) {
// 判断属性值的类型,如果是对象递归调用深拷贝
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
浅拷贝
数组的浅拷贝,可用concat、slice返回一个新数组的特性来实现拷贝for in
var arr = ['old', 1, true, null, undefined];
var new_arr = arr.concat(); // 或者var new_arr = arr.slice()也是一样的效果;
new_arr[0] = 'new';
console.log(arr); // ["old", 1, true, null, undefined]
console.log(new_arr); // ["new", 1, true, null, undefined]
-------------------------------------------
数组常用的方法
map此方法是将数组中的每个元素调用一个提供的函数,结果作为一个新的数组返回,并没有改变原来的数组
let arr = [1, 2, 3, 4, 5]
let newArr = arr.map(x => x*2)
//arr= [1, 2, 3, 4, 5] 原数组保持不变
//newArr = [2, 4, 6, 8, 10] 返回新数组
forEach此方法是将数组中的每个元素执行传进提供的函数,没有返回值,直接改变原数组,注意和map方法区分
let arr = [1, 2, 3, 4, 5]
num.forEach(x => x*2)
// arr = [2, 4, 6, 8, 10] 数组改变,注意和map区分
filter()此方法是将所有元素进行判断,将满足条件的元素作为一个新的数组返回
let arr = [1, 2, 3, 4, 5]
const isBigEnough => value => value >= 3
let newArr = arr.filter(isBigEnough )
//newNum = [3, 4, 5] 满足条件的元素返回为一个新的数组
reduce()此方法是所有元素调用返回函数,返回值为最后结果,传入的值必须是函数类型:
let arr = [1, 2, 3, 4, 5]
const add = (a, b) => a + b
let sum = arr.reduce(add)
//sum = 15 相当于累加的效果
与之相对应的还有一个 Array.reduceRight() 方法,区别是这个是从右向左操作的
push/pop
push:数组后面添加新元素,改变数组的长度
pop:数组删除最后一个元素 。 也改变长度
shift/unshift
shift:删除第一个元素 。 改变数组的长度
unshift:将一个或多个添加到数组开头 。 返回数组长度
isArray:返回bool
cancat:合并数组
toString:数组转字符串
join("--"):数组转字符串 。 间隔可以设置
splice(开始位置,删除个数,元素)万能方法 增删改
------------------------------------------------
判断是不是数组的方法
var arr = [1,2,3,1];
var arr2 = [{ abac : 1, abc : 2 }];
function isArrayFn(value){
if (typeof Array.isArray === "function") {
//判断是否支持isArray ie8之前不支持
return Array.isArray(value);
}else{
return Object.prototype.toString.call(value) === "[object Array]";
}
}
alert(isArrayFn(arr));// true
alert(isArrayFn(arr2));// true
前端面试常问的基础(七)
1.IE6或更低版本最多20个cookie
2.IE7和之后的版本最后可以有50个cookie。
3.Firefox最多50个cookie
4.chrome和Safari没有做硬性限制
IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。
优点:极高的扩展性和可用性
1.通过良好的编程,控制保存在cookie中的session对象的大小。
2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。
3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。
4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。
缺点:
1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。
2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。
3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。
在较高版本的浏览器中,js提供了sessionStorage和globalStorage。在HTML5中提供了localStorage来取代globalStorage。
html5中的Web Storage包括了两种存储方式:sessionStorage和localStorage。
sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。
而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
前端面试常问的基础(六)
- CSS3有哪些新特性?
- html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?
- 本地存储(Local Storage )和cookies(储存在用户本地终端上的数据)之间的区别是什么?
- 如何实现浏览器内多个标签页之间的通信?
- 你如何对网站的文件和资源进行优化?
- 什么是响应式设计?
- 新的 HTML5 文档类型和字符集是?
- HTML5 Canvas 元素有什么用?
- HTML5 存储类型有什么区别?
- 用H5+CSS3解决下导航栏最后一项掉下来的问题
- CSS3新增伪类有那些?
- 请用CSS实现:一个矩形内容,有投影,有圆角,hover状态慢慢变透明。
- 描述下CSS3里实现元素动画的方法
- html5\CSS3有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?
- 你怎么来实现页面设计图,你认为前端应该如何高质量完成工作? 一个满屏 品 字布局 如何设计?
- 你能描述一下渐进增强和优雅降级之间的不同吗?
- 为什么利用多个域名来存储网站资源会更有效?
- 请谈一下你对网页标准和标准制定机构重要性的理解。
- 请描述一下cookies,sessionStorage和localStorage的区别?
- 知道css有个content属性吗?有什么作用?有什么应用?
- 如何在 HTML5 页面中嵌入音频?
- 如何在 HTML5 页面中嵌入视频?
- HTML5 引入什么新的表单属性?
- CSS3新增伪类有那些?
- (写)描述一段语义的html代码吧。
- cookie在浏览器和服务器间来回传递。 sessionStorage和localStorage区别
- html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?
- 如何区分: DOCTYPE声明\新增的结构元素\功能元素
- 语义化的理解?
- HTML5的离线储存?
- 写出HTML5的文档声明方式
- HTML5和CSS3的新标签
- 自己对标签语义化的理解
前端面试常问的基础(五)
如何理解CSS的盒子模型?
每个HTML元素都是长方形盒子。 (1)盒子模型有两种:IE盒子模型、标准W3C盒子模型;IE的content部分包含了border和pading。 (2)标准W3C盒模型包含:内容(content)、填充(padding)、边界(margin)、边框(border)。
link属于XHTML标签,而@import是CSS提供的。 (2)页面被加载时,link会同时被加载,而@import引用的CSS会等到页面被加载完再加载。 (3)import只在IE 5以上才能识别,而link是XHTML标签,无兼容问题。 (4)link方式的样式权重高于@import的权重。 (5)使用dom控制样式时的差别。当使用javascript控制dom去改变样式的时候,只能使用link标签,因为@import不是dom可以控制的。
id选择器(# myid) 类选择器(.myclassname) 标签选择器(div、h1、p) 相邻选择器(h1 + p) 子选择器(ul < li) 后代选择器(li a) 通配符选择器( * ) 属性选择器(a[rel = "external"]) 伪类选择器(a: hover, li: nth - child) 可继承: font-size font-family color, UL LI DL DD DT;
不可继承 :border padding margin width height ;
优先级就近原则,样式定义最近者为准,载入样式以最后载入的定位为准。 优先级为: !important > id > class > tag important 比 内联优先级高 CSS3新增伪类举例: p:first-of-type 选择属于其父元素的首个<p>元素的每个<p>元素。 p:last-of-type 选择属于其父元素的最后<p>元素的每个<p>元素。 p:only-of-type 选择属于其父元素唯一的<p>元素的每个<p>元素。 p:only-child 选择属于其父元素的唯一子元素的每个<p>元素。 p:nth-child(2) 选择属于其父元素的第二个子元素的每个<p>元素。 :enabled、:disabled 控制表单控件的禁用状态。 :checked 单选框或复选框被选中。
(1)png24为的图片在IE6浏览器上出现背景,解决方案是做成PNG8。
(2)浏览器默认的margin和padding不同,解决方案是加一个全局的*{margin:0;padding:0;}来统一。
(3)IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE 6显示margin比设置的大。
(4)浮动ie产生的双边距问题:块级元素就加display:inline;行内元素转块级元素display:inline后面再加display:table。 .bb{
background-color:#f1ee18; /*所有识别*/
.background-color:#00deff\9; /*IE6、7、8识别*/
+background-color:#a200ff; /*IE6、7识别*/
_background-color:#1e0bd1; /*IE6识别*/ }
BFC,块级格式化上下文,一个创建了新的BFC的盒子是独立布局的,盒子里面的子元素的样式不会影响到外面的元素。在同一个 BFC 中的两个毗邻的块级盒在垂直方向(和布局方向有关系)的 margin 会发生折叠。
W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行布局,以及与其他元素的关系和相互作用。
display:none 隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,
就当他从来不存在。
visibility:hidden 隐藏对应的元素,但是在文档布局中仍保留原来的空间。
Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。
除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie。
但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生
浏览器的支持除了IE7及以下不支持外,其他标准浏览器都完全支持(ie及FF需在web服务器里运行),值得一提的是IE总是办好事,例如IE7、IE6中的UserData其实就是javascript本地存储的解决方案。通过简单的代码封装可以统一到所有的浏览器都支持web storage。
localStorage和sessionStorage都具有相同的操作方法,例如setItem、getItem和removeItem等
前端面试常问的基础(四)
将元素定义为网格容器,并为其内容建立新的 网格格式上下文。
值:
- grid :生成一个块级网格
- inline-grid :生成一个内联网格
在Bootstrap中,栅格系统将容器均分为12份,再调整内外边距,结合媒体查询,造就了这一强大的栅格系统。
flex布局
水平居中:
1. 行内元素,父元素 text-align : center
2. 块级元素有定宽,margin:0 auto;
3. 块级元素绝对定位,transform : translate(-50%,0);
4. 块级元素绝对定位,并且知道宽度, margin-left: - 宽度的一半
5. 块级元素绝对定位,left:0; right:0; margin:0 auto
垂直居中
1. 若元素是单行文本, 则可设置 line-height 等于父元素高度,原理见上面;
2. 若元素是行内块级元素, 基本思想是使用display: inline-block, vertical-align: middle和一个伪元素让内容块处于容器中央..parent::after, .son{ display:inline-block; vertical-align:middle; }
3. 使用flex, 在父元素上面添加.parent { display: flex; align-items: center;
4. 绝对定位的块用 transform: translate(0, -50%)
5. 绝对定位,并且有定高, margin-top : -高度的一半 注意不要用 margin-bottom, 不会生效的
6. 设置父元素相对定位(position:relative), 子元素如下css样式:.son{ position:absolute; height:固定; top:0; bottom:0; margin:auto 0; }
重绘重排
重绘是一个元素的外观变化所引发的浏览器行为;
重排是引起DOM树重新计算的行为;
1、回流/重排
渲染树的一部分必须要更新且节点的尺寸发生了变化,会触发重排操作。每个页面至少在初始化的时候会有一次重排操作。
2、重绘
部分节点需要更新,但没有改变其形状,会触发重绘操作。
会触发重绘或回流/重排的操作
1、添加、删除元素(回流+重绘)
2、隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)
3、移动元素,如改变top、left或移动元素到另外1个父元素中(重绘+回流)
4、改变浏览器大小(回流+重绘)
5、改变浏览器的字体大小(回流+重绘)
6、改变元素的padding、border、margin(回流+重绘)
7、改变浏览器的字体颜色(只重绘,不回流)
8、改变元素的背景颜色(只重绘,不回流)
深入浏览器理解CSS animations 和 transitions的性能问题
GPU的快在于:
- 绘制位图到屏幕上
- 一遍又一遍地绘制相同的位图
- 将同一位图绘制到不同位置,执行旋转以及缩放处理
GPU 的慢在于:
- 将位图加载到它的内存中
在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20+20次,总计40次计算。
在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。
transform为我们提供了丰富的api,例如scale,translate,rotate等等,但是在使用时需要考虑兼容性。但其实对于大多数css3来说,mobile端支持性较好,desktop端支持性需要格外注意。
物理像素(physical pixel)
即:设备像素(device pixel)。
本质是屏幕上的点,这个是跟设备有关系
CSS像素(css pixel)
指的是CSS样式代码中使用的逻辑像素(或者叫虚拟像素)。
软件要在设备上显示,css规定了长度单位(绝对单位和相对单位),比如:px 是一个 相对单位 ,相对的是 物理像素(physical pixel)
设备像素比(device pixel ratio) dpr
公式:物理像素数(硬件) / 逻辑像素数(软件),即(物理像素/CSS像素)。
在css中,可以通过 -webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio 和 -webkit-max-device-pixel-ratio 进行媒体查询,对不同dpr的设备,做一些样式适配。
如: dpr = 2 时,1个CSS像素 = 4个物理像素。因为像素点都是正方形,所以当1个CSS像素需要的物理像素增多2倍时,其实就是长和宽都增加了2倍
px em rem的区别
PX实际上就是像素,用PX设置字体大小时,比较稳定和精确。但是这种方法存在一个问题,当用户在浏览器中浏览我们制作的Web页面时,如果改变了浏览器的缩放,这时会使用我们的Web页面布局被打破。这样对于那些关心自己网站可用性的用户来说,就是一个大问题了。因此,这时就提出了使用“em”来定义Web页面的字体。
EM就是根据基准来缩放字体的大小。EM实质是一个相对值,而非具体的数值。这种技术需要一个参考点,一般都是以<body>的“font-size”为基准。如WordPress官方主题Twenntytwelve的基准就是14px=1em。
另外,em是相对于父元素的属性而计算的,如果想计算px和em之间的换算,输入数据就可以px和em相互计算。
Rem是相对于根元素<html>,这样就意味着,我们只需要在根元素确定一个参考值。
前端面试常问的基础(三)
- JS中浮点数精度误差解决
- 如果有精度要求,可以用toFixed方法处理
- 通用处理方案:把需要计算的数字乘以 10 的 n 次幂,换算成计算机能够精确识别的整数,然后再除以 10 的 n 次幂
- promises,observables,generator 或 async-wait 编所需的函数。
- JavaScript Proxy实现简单的数据绑定
- “new” 关键字在 JavaScript 中有什么作用?
new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。
通过new可以产生原对象的一个实例对象,而这个实例对象继承了原对象的属性和方法。因此, new存在的意义在于它实现了javascript中的继承,而不仅仅是实例化了一个对象!
- JavaScript 中有哪些不同的函数调用模式? 详细解释。 提示:有四种模式,函数调用,方法调用,.call() 和 .apply()。
- 函数模式 fn()
- 方法模式 a.fn()
- 构造器模式 new
- 上下文模式 call apply
/*apply()方法*/两个参数 function.apply(thisObj[, argArray]) /*call()方法*/多个参数 function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);
- 新 ECMAScript 提案
https://www.cnblogs.com/fundebug/p/what-is-new-in-javascript-for-2019.html
bigint
class 增加静态方法和属性 私有属性和方法
symbol值在序列化的过程中会被忽略或被转换成null
Fetch API 相对于传统的 Ajax 有哪些改进?
改进:promise 风格的api,async/await方式调用更友好,更简洁,错误处理更直观
缺点/难点:
- fetch 是一种底层的 api,json传值必须转换成字符串,并且设置content-Type为application/json
- fetch 默认情况下不会发送 cookie
- 无法获取progress,也就是说无法用fetch做出有进度条的请求
- 不能中断,我们知道传统的xhr是可以通过调用abort方法来终止我们的请求的
其实javasript的社区一直很活跃,相信上述问题很快会在未来的更新中解决
前端面试常问的基础(二)
1. 一个程序至少有一个进程,一个进程至少有一个线程
2. 线程的划分尺度小于进程,使得多线程程序的并发性高
3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
------------
1.IE6或更低版本最多20个cookie
2.IE7和之后的版本最后可以有50个cookie。
3.Firefox最多50个cookie
4.chrome和Safari没有做硬性限制
IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。
优点:极高的扩展性和可用性
1.通过良好的编程,控制保存在cookie中的session对象的大小。
2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。
3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。
4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。
缺点:
1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。
2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。
3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。
本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326
待完善
收起阅读 »前端面试常问的基础(一)
IE 盒子模型、标准 W3C 盒子模型;IE的content部分包含了 border 和 padding;
new操作符具体干了什么呢?
1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
2. 属性和方法被加入到 this 引用的对象中
3. 新创建的对象由 this 所引用,并且最后隐式的返回 this
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小
内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。
1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。
2. 闭包
3. 控制台日志
4. 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)
一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
分为4个步骤:
1. 当发送一个 URL 请求时,不管这个 URL 是 Web 页面的 URL 还是 Web 页面上每个资源的 URL,浏览器都会开启一个线程来处理这个请求,同时在远程 DNS 服务器上启动一个 DNS 查询。这能使浏览器获得请求对应的 IP 地址。
2. 浏览器与远程 Web 服务器通过 TCP 三次握手协商来建立一个 TCP/IP 连接。该握手包括一个同步报文,一个同步-应答报文和一个应答报文,这三个报文在 浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,而后服务器应答并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。
3. 一旦 TCP/IP 连接建立,浏览器会通过该连接向远程服务器发送 HTTP 的 GET 请求。远程服务器找到资源并使用 HTTP 响应返回该资源,值为 200 的 HTTP 响应状态表示一个正确的响应。
4. 此时,Web 服务器提供资源服务,客户端开始下载资源。
GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符
POST:一般用于修改服务器上的资源,对所发送的信息没有限制
Ajax 同步和异步的区别:
1. 同步:提交请求 -> 等待服务器处理 -> 处理完毕返回,这个期间客户端浏览器不能干任何事
2. 异步:请求通过事件触发 -> 服务器处理(这是浏览器仍然可以作其他事情)-> 处理完毕
js数组去重
[1,1,2,2,3,3,3,3].filter(function(elem, index, self) {
///结果是true的时候返回后面的值
return index == self.indexOf(elem);
})
1. XSS
2. sql注入
3. CSRF:是跨站请求伪造,很明显根据刚刚的解释,他的核心也就是请求伪造,通过伪造身份提交POST和GET请求来进行跨域的攻击
完成CSRF需要两个步骤:
1. 登陆受信任的网站A,在本地生成 COOKIE
2. 在不登出A的情况下,或者本地 COOKIE 没有过期的情况下,访问危险网站B。
2.HTTP 报文的组成部分
请求报文
1.请求行:http方法、页面地址、协议、版本
2.请求头:key、value告诉服务端需要内容,注意什么类型
3.空行:告诉服务端请求头已经结束
4.请求体
响应报文
1.状态行:协议、版本、状态码
2.响应头
3.空行
4.响应体:文档部分
- TCP/IP 四层协议: 应用层、传输层、网络互连层和主机到网络层. http对应应用层
- ISO 七层模型: 物理层, 数据链路层, 网络层, 传输层, 会话层, 表示层, 应用层. http对应应用
流行的一些东西:
1. Node.js
2. Mongodb
3. npm
4. MVVM
5. MEAN
6. three.js
7. React
本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326
解决js精度丢失办法
很简单一个问题,0.1+0.2,我们肉眼可见的算出来等于0.3,但js是一个神奇的语言,我们在控制台输入0.1+0.2等于0.30000000000000004,为什么会这样尼,我百度了了一下,原因如下:
JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3。
原因就是这么个奇葩,做需求的时候涉及到数字计算,那就得解决它老人家这个毛病,解决这个问题,我一般会封装成一个文件,到后面需要的地方可以模块化引入,并使用
1.判断obj是否为一个整数
export const isInteger = (obj) => {
return Math.floor(obj) === obj //向下取整就是为了让整数部分截取下来不变
}
2.将一个浮点数转成整数,返回整数和倍数
比如:3.14 -->314,倍数是 100 ,floatNum {number} 小数,返回一个对象, {times:100, num: 314}
export const toInteger = (floatNum) => {
var ret = {times: 1, num: 0};
if (isInteger(floatNum)) {
ret.num = floatNum;
return ret
}
//1.//转字符串
var strfi = floatNum + '';
//2.//拿到小数点为
var dotPos = strfi.indexOf('.');
//3. //截取需要的长度
var len = strfi.substr(dotPos + 1).length;
//4.倍数就是长度的幂
var times = Math.pow(10, len);
var intNum = parseInt(floatNum * times , 10);
ret.times = times;
ret.num = intNum;
return ret
}
3.把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
- 参数:a {number} 运算数1
- b:{number} 运算数2,
- op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
export const operation = (a, b, op) => {
var o1 = toInteger(a);
var o2 = toInteger(b);
var n1 = o1.num;
var n2 = o2.num;
var t1 = o1.times;
var t2 = o2.times;
var max = t1 > t2 ? t1 : t2;
var result = null;
switch (op) {
case 'add':
if (t1 === t2) { // 两个小数位数相同
result = n1 + n2
} else if (t1 > t2) { // o1 小数位 大于 o2
result = n1 + n2 * (t1 / t2)
} else { // o1 小数位 小于 o2
result = n1 * (t2 / t1) + n2
}
return result / max;
case 'subtract':
if (t1 === t2) {
result = n1 - n2
} else if (t1 > t2) {
result = n1 - n2 * (t1 / t2)
} else {
result = n1 * (t2 / t1) - n2
}
return result / max;
case 'multiply':
result = (n1 * n2) / (t1 * t2);
return result;
case 'divide':
result = (n1 / n2) * (t2 / t1);
return result
}
}
原文:https://segmentfault.com/a/1190000022730047
ES6 exports 与 import 使用
在创建JavaScript模块时,export
用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import
使用它们。
被导出的绑定值依然可以在本地进行修改。
在使用import 进行导入时,这些绑定值只能被导入模块所读取,但在 export 导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。
exports
ES6模块只支持静态导出,只可以在模块的最外层作用域使用export
,不可在条件语句与函数作用域中使用。
Named exports (命名导出)
这种方式主要用于导出多个函数或者变量, 明确知道导出的变量名称。
使用:只需要在变量或函数前面加 export
关键字即可。
使用场景:比如 utils、tools、common 之类的工具类函数集,或者全站统一变量
- export 后面不可以是表达式,因为表达式只有值,没有名字。
- 每个模块包含任意数量的导出。
// lib.js
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
// index.js 使用方式1
import { square, diag } from 'lib';
console.log(square(11)); // 121
// index.js 使用方式2
import * as lib from 'lib';
console.log(lib.square(11)); // 121
简写格式,统一列出需要输出的变量,例如上面的lib.js可以改写成
// lib.js
const sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function add (x, y) {
return x + y;
}
export { sqrt, square, add };
Default exports (默认导出)
这种方式主要用于导出类文件或一个功能比较单一的函数文件;
使用:只需要在变量或函数前面加 export default
关键字即可。
- 每个模块最多只能有一个默认导出;
- 默认导出可以视为名字是
default
的模块输出变量;- 默认导出后面可以是表达式,因为它只需要值。
导出一个值:
export default 123;
导出一个函数:
// myFunc.js
export default function () { ... };
// index.js
import myFunc from 'myFunc';
myFunc();
导出一个类:
// MyClass.js
class MyClass{
constructor() {}
}
export default MyClass;
// 或者
export { MyClass as default, … };
// index.js
import MyClass from 'MyClass';
export default 与 export 的区别:
- 不需要知道导出的具体变量名;
- 导入【import】时不需要 { } 包裹;
Combinations exports (混合导出)
混合导出是 Named exports
和 Default exports
组合导出。
混合导出后,默认导入一定放在命名导入前面;
// lib.js
export const myValue = '';
export const MY_CONST = '';
export function myFunc() {
...
}
export function* myGeneratorFunc() {
...
}
export default class MyClass {
...
}
// index.js
import MyClass, { myValue, myFunc } from 'lib';
Re-exporting (别名导出)
一般情况下,export 导出的变量名是原文件中的变量名,但也可以用 as 关键字来指定别名。这样做是为了简化或者语义化 export 的函数名。
同一个变量允许使用不同名字输出多次
// lib.js
function getName() {
...
};
function setName() {
...
};
export {
getName as get,
getName as getUserName,
setName as set
}
Module Redirects (中转模块导出)
为了方便使用模块导入,在一个父模块中“导入-导出”不同模块。简单来说:创建单个模块,集中多个模块的多个导出。
使用:使用 export from
语法实现;
export * from 'lib'; // 没有设置 export default
export * as myFunc2 from 'myFunc'; // 【ES2021】没有设置 export default
export { default as function1, function2 } from 'bar.js';
上述例子联合使用导入和导出:
import { default as function1, function2 } from 'bar.js';
export { function1, function2 };
尽管此时 export 与 import 等效,但以下语法在语法上无效:
import DefaultExport from 'bar.js'; // 有效的
export DefaultExport from 'bar.js'; // 无效的
正确的做法是重命名这个导出:
export { default as DefaultExport } from 'bar.js';
Importing
// Named imports
import { foo, bar as b } from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';
// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, { foo, bar as b } from './some-module.mjs';
// Empty import (for modules with side effects)
import './some-module.mjs';
原文:https://segmentfault.com/a/1190000039957496
常见的8个前端防御性编程方案
关于前端防御性编程
- 我们大多数情况可能遇到过,后端的由于同时请求人数过多,或者数据量过大,又或者是因为异常导致服务异常,接口请求失败,然后前端出现白屏或者报错
- 还有一种情况,是前端自身写的代码存在一些缺陷,整个系统不够健壮,从而会出现白屏,或者业务系统异常,用户误操作等
- 那么,就出现了前端防御性编程
常见的问题和防范
1.最常见的问题:
uncaught TypeError: Cannot read property 'c' of undefined
出现这个问题最根本原因是:
当我们初始化一个对象obj为{}时候,obj.a这个时候是undefined.我们打印obj.a可以得到undefined,但是我们打印obj.a.c的时候,就会出现上面的错误。js对象中的未初始化属性值是undefined,从undefined读取属性就会导致这个错误(同理,null也一样)
如何避免?
js和ts目前都出现了一个可选链概念
,例如:
const obj = {};
console.log(obj?.b?.c?.d)
上面的代码并不会报错,原因是?.
遇到是空值的时候便会返回undefined.
2.前端接口层面的错误机制捕获
前端的接口调用,一般都比较频繁,我们这时候可以考虑使用单例模式,将所有的axios请求都用一个函数封装一层。统一可以在这个函数中catch捕获接口调用时候的未知错误,伪代码如下:
function ajax(url,data,method='get'){
const promise = axios[method](url,data)
return promise.then(res=>{
}).catch(error){
//统一处理错误
}
}
那么只要发生接口调用的未知错误都会在这里被处理了
3.错误边界(Error Boundaries,前端出现未知错误时,展示预先设定的UI界面)
以React为例
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
使用示例:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
注意
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多)
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
4.前端复杂异步场景导致的错误
这个问题可能远不止这么简单,但是大道至简,遵循单向数据流的方式去改变数据,例如:
//test.js
export const obj = {
a:1,
b:2
}
//使用obj
import {obj} from './test.js';
obj.a=3;当你频繁使用这个
obj
对象时,你无法根据代码去知道它的改变顺序(即在某个时刻它的值是什么),而且这里面可能存在不少异步的代码,当我们换一种方式,就能知道它的改变顺序了,也更方便我们debug
例如://test.js
export const obj = {
a:1,
b:2
}
export function setObj (key,value) {
console.log(key,value)
obj[key] = value
}这样,我们就做到了
5.前端专注“前端”
- 对于一些敏感数据,例如登录态,鉴权相关的。前端应该是尽量做无感知的转发、携带(这样也不会出现安全问题)
6.页面做到可降级
- 对于一些刚上新的业务,或者有存在风险的业务模块,或者会调取不受信任的接口,例如第三方的接口,这个时候就要做一层降级处理,例如接口调用失败后,剔除对应模块的展示,让用户无感知的使用
7.巧用loading和disabled
- 用户操作后,要及时loading和disabled确保不让用户进行重复,防止业务侧出现bug
8.慎用innerHTML
- 容易出现安全漏洞,例如接口返回了一段JavaScript脚本,那么就会立即执行。此时脚本如果是恶意的,那么就会出现不可预知的后果,特别是电商行业,尤其要注意
嗨,你真的懂this吗?
this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JsvaScript开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?
请先回答第一个问题:如何准确判断this指向的是什么?【面试的高频问题】
【图片来源于网络,侵删】
再看一道题,控制台打印出来的值是什么?【浏览器运行环境】
var number = 5;
var obj = {
number: 3,
fn1: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);
如果你思考出来的结果,与在浏览中执行结果相同,并且每一步的依据都非常清楚,那么,你可以选择继续往下阅读,或者关闭本网页,愉快得去玩耍。如果你有一部分是靠蒙的,或者对自己的答案并不那么确定,那么请继续往下阅读。
毕竟花一两个小时的时间,把this彻底搞明白,是一件很值得事情,不是吗?
本文将细致得讲解this的绑定规则,并在最后剖析前文两道题。
为什么要学习this?
首先,我们为什么要学习this?
- this使用频率很高,如果我们不懂this,那么在看别人的代码或者是源码的时候,就会很吃力。
- 工作中,滥用this,却没明白this指向的是什么,而导致出现问题,但是自己却不知道哪里出问题了。【在公司,我至少帮10个以上的开发人员处理过这个问题】
- 合理的使用this,可以让我们写出简洁且复用性高的代码。
- 面试的高频问题,回答不好,抱歉,出门右拐,不送。
不管出于什么目的,我们都需要把this这个知识点整的明明白白的。
OK,Let's go!
this是什么?
言归正传,this是什么?首先记住this不是指向自身!this 就是一个指针,指向调用函数的对象。这句话我们都知道,但是很多时候,我们未必能够准确判断出this究竟指向的是什么?这就好像我们听过很多道理 却依然过不好这一生。今天咱们不探讨如何过好一生的问题,但是呢,希望阅读完下面的内容之后,你能够一眼就看出this指向的是什么。
为了能够一眼看出this指向的是什么,我们首先需要知道this的绑定规则有哪些?
- 默认绑定
- 隐式绑定
- 硬绑定
- new绑定
上面的名词,你也许听过,也许没听过,但是今天之后,请牢牢记住。我们将依次来进行解析。
默认绑定
默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
function sayHi(){
console.log('Hello,', this.name);
}
var name = 'YvetteLau';
sayHi();
在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。
上面的代码,如果在浏览器环境中运行,那么结果就是 Hello,YvetteLau
但是如果在node环境中运行,结果就是 Hello,undefined.这是因为node中name并不是挂在全局对象上的。
本文中,如不特殊说明,默认为浏览器环境执行结果。
隐式绑定
函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun().我们来看一段代码:
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();
打印的结果是 Hello,YvetteLau.
sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person)
需要注意的是:对象属性链中只有最后一层会影响到调用位置。
function sayHi(){
console.log('Hello,', this.name);
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var person1 = {
name: 'YvetteLau',
friend: person2
}
person1.friend.sayHi();
结果是:Hello, Christina.
因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend。
隐式绑定有一个大陷阱,绑定很容易丢失(或者说容易给我们造成误导,我们以为this指向的是什么,但是实际上并非如此).
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();
结果是: Hello,Wiliam.
这是为什么呢,Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢继续这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定,但是也不一定就是默认绑定,这里有点小疑问,我们后来会说到。
除了上面这种丢失之外,隐式绑定的丢失是发生在回调函数中(事件回调也是其中一种),我们来看下面一个例子:
function sayHi(){
console.log('Hello,', this.name);
}
var person1 = {
name: 'YvetteLau',
sayHi: function(){
setTimeout(function(){
console.log('Hello,',this.name);
})
}
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
person2.sayHi();
},200);
结果为:
Hello, Wiliam
Hello, Wiliam
Hello, Christina
- 第一条输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
- 第二条输出是不是有点迷惑了?说好XXX.fun()的时候,fun中的this指向的是XXX呢,为什么这次却不是这样了!Why?
其实这里我们可以这样理解: setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。
- 第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。
读到这里,也许你已经有点疲倦了,但是答应我,别放弃,好吗?再坚持一下,就可以掌握这个知识点了。
显式绑定
显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。(注意:《你不知道的Javascript》中将bind单独作为了硬绑定讲解了)
call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)
输出的结果为: Hello, YvetteLau. 因为使用硬绑定明确将this绑定在了person上。
那么,使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn();
}
Hi.call(person, person.sayHi);
输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。
现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn.call(this);
}
Hi.call(person, person.sayHi);
此时,输出的结果为: Hello, YvetteLau,因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。
至此,革命已经快胜利了,我们来看最后一种绑定规则: new 绑定。
new 绑定
javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”。
使用new来调用函数,会自动执行下面的操作:
- 创建一个新对象
- 将构造函数的作用域赋值给新对象,即this指向这个新对象
- 执行构造函数中的代码
- 返回新对象
因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。
function sayHi(name){
this.name = name;
}
var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name);
输出结果为 Hello, Yevtte, 原因是因为在var Hi = new sayHi('Yevtte');这一步,会将sayHi中的this绑定到Hi对象上。
绑定优先级
我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办?
显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
这个规则时如何得到的,大家如果有兴趣,可以自己写个demo去测试,或者记住上面的结论即可。
绑定例外
凡事都有例外,this的规则也是这样。
如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar() {
console.log(this.name);
}
bar.call(null); //Chirs
输出的结果是 Chirs,因为这时实际应用的是默认绑定规则。
箭头函数
箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:
(1)函数体内的this对象,继承的是外层代码块的this。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
(5)箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向.
OK,我们来看看箭头函数的this是什么?
var obj = {
hi: function(){
console.log(this);
return ()=>{
console.log(this);
}
},
sayHi: function(){
return function() {
console.log(this);
return ()=>{
console.log(this);
}
}
},
say: ()=>{
console.log(this);
}
}
let hi = obj.hi(); //输出obj对象
hi(); //输出obj对象
let sayHi = obj.sayHi();
let fun1 = sayHi(); //输出window
fun1(); //输出window
obj.say(); //输出window
那么这是为什么呢?如果大家说箭头函数中的this是定义时所在的对象,这样的结果显示不是大家预期的,按照这个定义,say中的this应该是obj才对。
我们来分析一下上面的执行结果:
- obj.hi(); 对应了this的隐式绑定规则,this绑定在obj上,所以输出obj,很好理解。
- hi(); 这一步执行的就是箭头函数,箭头函数继承上一个代码库的this,刚刚我们得出上一层的this是obj,显然这里的this就是obj.
- 执行sayHi();这一步也很好理解,我们前面说过这种隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window.
- fun1(); 这一步执行的是箭头函数,如果按照之前的理解,this指向的是箭头函数定义时所在的对象,那么这儿显然是说不通。OK,按照箭头函数的this是继承于外层代码库的this就很好理解了。外层代码库我们刚刚分析了,this指向的是window,因此这儿的输出结果是window.
- obj.say(); 执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window.
你说箭头函数的this是静态的?
依旧是前面的代码。我们来看看箭头函数中的this真的是静态的吗?
我要说:非也
var obj = {
hi: function(){
console.log(this);
return ()=>{
console.log(this);
}
},
sayHi: function(){
return function() {
console.log(this);
return ()=>{
console.log(this);
}
}
},
say: ()=>{
console.log(this);
}
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); //输出window
fun1(); //输出window
let fun2 = sayHi.bind(obj)();//输出obj
fun2(); //输出obj
可以看出,fun1和fun2对应的是同样的箭头函数,但是this的输出结果是不一样的。
所以,请大家牢牢记住一点: 箭头函数没有自己的this,箭头函数中的this继承于外层代码库中的this.
总结
关于this的规则,至此,就告一段落了,但是想要一眼就能看出this所绑定的对象,还需要不断的训练。
我们来回顾一下,最初的问题。
1. 如何准确判断this指向的是什么?
- 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
- 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
- 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
- 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
- 如果把Null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 如果是箭头函数,箭头函数的this继承的是外层代码块的this。
2. 执行过程解析
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
我们来分析一下,这段代码的执行过程。
1.在定义obj的时候,fn对应的闭包就执行了,返回其中的函数,执行闭包中代码时,显然应用不了new绑定(没有出现new 关键字),硬绑定也没有(没有出现call,apply,bind关键字),隐式绑定有没有?很显然没有,如果没有XX.fn(),那么可以肯定没有应用隐式绑定,所以这里应用的就是默认绑定了,非严格模式下this绑定到了window上(浏览器执行环境)。【这里很容易被迷惑的就是以为this指向的是obj,一定要注意,除非是箭头函数,否则this跟词法作用域是两回事,一定要牢记在心】
window.number * = 2; //window.number的值是10(var number定义的全局变量是挂在window上的)
number = number * 2; //number的值是NaN;注意我们这边定义了一个number,但是没有赋值,number的值是undefined;Number(undefined)->NaN
number = 3; //number的值为3
2.myFun.call(null);我们前面说了,call的第一个参数传null,调用的是默认绑定;
fn: function(){
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
执行时:
var num = this.number; //num=10; 此时this指向的是window
this.number * = 2; //window.number = 20
console.log(num); //输出结果为10
number *= 3; //number=9; 这个number对应的闭包中的number;闭包中的number的是3
console.log(number); //输出的结果是9
3.obj.fn();应用了隐式绑定,fn中的this对应的是obj.
var num = this.number;//num = 3;此时this指向的是obj
this.number *= 2; //obj.number = 6;
console.log(num); //输出结果为3;
number *= 3; //number=27;这个number对应的闭包中的number;闭包中的number的此时是9
console.log(number);//输出的结果是27
4.最后一步console.log(window.number);输出的结果是20
因此组中结果为:
10
9
3
27
20
严格模式下结果,大家可以根据今天所学,自己分析,巩固一下知识点。
最后,恭喜坚持读完的小伙伴们,你们成功get到了this这个知识点,但是想要完全掌握,还是要多回顾和练习。如果你有不错的this练习题,欢迎在评论区留言哦,大家一起进步!
原文:https://segmentfault.com/a/1190000018630013
收起阅读 »前端基础-你真的懂函数吗?
前言
众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知
正文
1.箭头函数
ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:
let arrowSum = (a, b) => {
return a + b;
};
let functionExpressionSum = function(a, b) {
return a + b;
};
console.log(arrowSum(5, 8)); // 13
console.log(functionExpressionSum(5, 8)); // 13
使用箭头函数须知:
- 箭头函数的函数体如果不用大括号括起来会隐式返回这行代码的值
- 箭头函数不能使用
arguments
、super
和new.target
,也不能用作构造函数 - 箭头函数没有
prototype
属性
2.函数声明与函数表达式
JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升
(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:
// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
上述代码的报错有一些同学可能认为是let导致的暂时性死区
。其实原因并不出在这里,这是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到let
的那一行,那么执行上下文中就没有函数的定义。大家可以自己尝试一下,就算是用var
来定义,也是一样会出错。
3.函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments
和 this
。ECMAScript 6 又新增了 new.target
属性。
arguments
它是一个类数组对象
,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
// 上述代码可以运用arguments来进行解耦
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
这个重写之后的 factorial()函数已经用 arguments.callee
代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。
arguments.callee 的解耦示例
let trueFactorial = factorial;
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
这里 factorial 函数在赋值给trueFactorial后被重写了 那么我们如果在递归中不使用arguments.callee
那么显然trueFactorial(5)的运行结果也是0,但是我们解耦之后,新的变量还是可以正常的进行
this
函数内部另一个特殊的对象是 this
,它在标准函数和箭头函数中有不同的行为。
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。
在箭头函数中,this引用的是定义箭头函数的上下文。
caller
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller指向 outer()。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();
new.target
ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
这里可以做一些延申,还有没有其他办法来判断函数是否通过new来调用的呢?
可以使用 instanceof
来判断。我们知道在new
的时候发生了哪些操作?用如下代码表示:
var p = new Foo()
// 实际上执行的是
// 伪代码
var o = new Object(); // 或 var o = {}
o.__proto__ = Foo.prototype
Foo.call(o)
return o
上述伪代码在MDN是这么说的:
- 一个继承自 Foo.prototype 的新对象被创建。
- 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
- 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
new
的操作说完了 现在我们看一下 instanceof
,MDN上是这么说的:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
也就是说,A的N个__proto__ 全等于 B.prototype,那么A instanceof B返回true
,现在知识点已经介绍完毕,可以开始上代码了
function Person() {
if (this instanceof Person) {
console.log("通过new 创建");
return this;
} else {
console.log("函数调用");
}
}
const p = new Person(); // 通过new创建
Person(); // 函数调用
解析:我们知道new构造函数的this指向实例,那么上述代码不难得出以下结论this.__proto__ === Person.prototype
。所以这样就可以判断函数是通过new还是函数调用
这里我们其实还可以将 this instanceof Person
改写为 this instanceof arguments.callee
4.闭包
终于说到了闭包,闭包这玩意真的是面试必问,所以掌握还是很有必要的
闭包
指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
上述代码中,由于foo函数内部的bar函数使用了foo函数内部的变量,并且bar函数return把变量return了出去,这样闭包就产生了,这使得我们可以在外部拿到这些变量。
const b = foo();
b() // 50
foo函数在调用的时候创建了一个执行上下文,可以在此上下文中使用a,b变量,理论上说,在foo调用结束,函数内部的变量会v8引擎的垃圾回收机制通过特定的标记回收。但是在这里,由于闭包的产生,a,b变量并不会被回收,这就导致我们在全局上下文(或其他执行上下文)中可以访问到函数内部的变量。
我之前看到了一个说法:
无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包,闭包包含在函数创建时作用域中的所有变量,类似于背包,函数定义附带一个小背包,它的包中存储了函数定义时作用域中的所有变量
以此引申出一个经典面试题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
怎样可以使得上述代码的输出变为1,2,3,4,5?
使用es6我们可以很简单的做出解答:将var换成let。
那么我们使用刚刚学到的闭包知识怎么来解答呢?代码如下:
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
根据上面的说法,将闭包看成一个背包,背包中包含定义时的变量,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值,即可解决。
5.立即调用的函数表达式(IIFE)
如下就是立即调用函数表达式
(function() {
// 块级作用域
})();
使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 抛出错误
ES6的块级作用域:
// 内嵌块级作用域
{
let i;
for (i = 0; i < count; i++) {
console.log(i);
}
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) {
console.log(i);
}
console.log(i); // 抛出错误
IIFE的另一个作用就是上文中的解决settimeout的输出问题
附录知识点
关于instanceof
Function instanceof Object;//true
Object instanceof Function;//true
上述代码大家可以尝试在浏览器中跑一下,非常的神奇,那么这是什么原因呢?
借用大佬的一张图
//构造器Function的构造器是它自身
Function.constructor=== Function;//true
//构造器Object的构造器是Function(由此可知所有构造器的constructor都指向Function)
Object.constructor === Function;//true
//构造器Function的__proto__是一个特殊的匿名函数function() {}
console.log(Function.__proto__);//function() {}
//这个特殊的匿名函数的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype//true
//Object的__proto__指向Function的prototype,也就是上面中所述的特殊匿名函数
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true
结论:
- 所有的构造器的constructor都指向Function
- Function的prototype指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向Object.prototype
结尾
本文主要参考 《JavaScript 高级程序设计 第四版》 由于作者水平有限,如有错误,敬请与我联系,谢谢您的阅读!
原文:https://segmentfault.com/a/1190000039904453
什么,项目构建时内存溢出了?了解一下 node 内存限制
背景
在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出
的问题。
当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。
今天听同事分享了一个新方法
,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。
正文
但 Node 进程的内存限制
会是多少呢?
在网上查阅了到如下描述:
Currently, by default V8 has a memory limit of 512mb on 32-bit systems, and 1gb on 64-bit systems. The limit can be raised by setting --max-old-space-size to a maximum of ~1gb (32-bit) and ~1.7gb (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.
翻译一下:
当前,默认情况下,V8在32位系统上的内存限制为512mb,在64位系统上的内存限制为1gb。
可以通过将
--max-old-space-size
设置为最大〜1gb(32位)和〜1.7gb(64位)来提高此限制,但是如果达到内存限制, 建议您将单个进程
拆分为多个工作进程
。
如果你想知道自己电脑的内存限制有多大, 可以直接把内存撑爆, 看报错。
运行如下代码:
// Small program to test the maximum amount of allocations in multiple blocks.
// This script searches for the largest allocation amount.
// Allocate a certain size to test if it can be done.
function alloc (size) {
const numbers = size / 8;
const arr = []
arr.length = numbers; // Simulate allocation of 'size' bytes.
for (let i = 0; i < numbers; i++) {
arr[i] = i;
}
return arr;
};
// Keep allocations referenced so they aren't garbage collected.
const allocations = [];
// Allocate successively larger sizes, doubling each time until we hit the limit.
function allocToMax () {
console.log("Start");
const field = 'heapUsed';
const mu = process.memoryUsage();
console.log(mu);
const gbStart = mu[field] / 1024 / 1024 / 1024;
console.log(`Start ${Math.round(gbStart * 100) / 100} GB`);
let allocationStep = 100 * 1024;
// Infinite loop
while (true) {
// Allocate memory.
const allocation = alloc(allocationStep);
// Allocate and keep a reference so the allocated memory isn't garbage collected.
allocations.push(allocation);
// Check how much memory is now allocated.
const mu = process.memoryUsage();
const gbNow = mu[field] / 1024 / 1024 / 1024;
console.log(`Allocated since start ${Math.round((gbNow - gbStart) * 100) / 100} GB`);
}
// Infinite loop, never get here.
};
allocToMax();
我的电脑是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,这段代码大概在 1.6 GB 左右内存时候抛出异常。
那我们现在知道 Node Process 确实是有一个内存限制的, 那我们就来增大它的内存限制再试一下。
用 node --max-old-space-size=6000
来运行这段代码,得到如下结果:
内存达到 4.6G 的时候也溢出了。
你可能会问, node 不是有内存回收吗?这个我们在下面会讲。
使用这个参数:node --max-old-space-size=6000
, 我们增加的内存中老生代区域
的大小,比较暴力。
就像上文中提到的: 如果达到内存限制, 建议您将单个进程
拆分为多个工作进程
。
这个项目是一个 ts 项目,ts 文件的编译是比较占用内存的,如果把这部分独立成一个单独的进程, 情况也会有所改善。
因为 ts-loader
内部调用了 tsc
,在使用 ts-loader 时,会使用 tsconfig.js配置文件。
当项目中的代码变的越来越多,体积也越来越庞大时,项目编译时间
也随之增加。
这是因为 Typescript 的语义检查器
必须在每次重建时检查所有文件
。
ts-loader
提供了一个 transpileOnly
选项,它默认为 false
,我们可以把它设置为 true
,这样项目编译时就不会进行类型检查,也不会输出声明文件。
对一下 transpileOnly
分别设置 false
和 true
的项目构建速度对比:
- 当 transpileOnly 为 false 时,整体构建时间为 4.88s.
- 当 transpileOnly 为 true 时,整体构建时间为 2.40s.
虽然构建速度提升了,但是有了一个弊端: 打包编译不会进行类型检查
。
好在官方推荐了这样一个插件, 提供了这样的能力: fork-ts-checker-webpack-plugin
。
官方示例的使用也非常简单:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
...
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}
在我这个实际的项目中,vue.config.js
修改如下:
configureWebpack: config => {
// get a reference to the existing ForkTsCheckerWebpackPlugin
const existingForkTsChecker = config.plugins.filter(
p => p instanceof ForkTsCheckerWebpackPlugin,
)[0];
// remove the existing ForkTsCheckerWebpackPlugin
// so that we can replace it with our modified version
config.plugins = config.plugins.filter(
p => !(p instanceof ForkTsCheckerWebpackPlugin),
);
// copy the options from the original ForkTsCheckerWebpackPlugin
// instance and add the memoryLimit property
const forkTsCheckerOptions = existingForkTsChecker.options;
forkTsCheckerOptions.memoryLimit = 4096;
config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions));
}
修改之后, 构建就成功了。
关于垃圾回收
在 Node.js 里面,V8 自动帮助我们进行垃圾回收, 让我们简单看一下V8中如何处理内存。
一些定义
常驻集大小:是RAM中保存的进程所占用的内存部分,其中包括:
- 代码本身
- 栈
- 堆
- stack:包含原始类型和对对象的引用
- 堆:存储引用类型,例如对象,字符串或闭包
- 对象的浅层大小:对象本身持有的内存大小
- 对象的保留大小:删除对象及其相关对象后释放的内存大小
垃圾收集器如何工作
垃圾回收是回收由应用程序不再使用的对象所占用的内存的过程。
通常,内存分配很便宜,而内存池用完时收集起来很昂贵。
如果无法从根节点访问对象,则该对象是垃圾回收的候选对象,因此该对象不会被根对象或任何其他活动对象引用。
根对象可以是全局对象,DOM元素或局部变量。
堆有两个主要部分,即 New Space
和 Old Space
。
新空间是进行新分配的地方。
在这里收集垃圾的速度很快,大小约为1-8MB
。
留存在新空间中的物体被称为新生代
。
在新空间中幸存下来的物体被提升的旧空间-它们被称为老生代
。
旧空间中的分配速度很快,但是收集费用很高,因此很少执行。
node 垃圾回收
Why is garbage collection expensive?
The V8 JavaScript engine employs a stop-the-world garbage collector mechanism.
In practice, it means that the program stops execution while garbage collection is in progress.
通常,约20%的年轻一代可以存活到老一代,旧空间的收集工作将在耗尽后才开始。
为此,V8 引擎使用两种不同的收集算法
:
- Scavenge: 速度很快,可在
新生代
上运行, - Mark-Sweep: 速度较慢,并且可以在
老生代
上运行。
篇幅有限,关于v8垃圾回收的更多信息,可以参考如下文章:
总结
小小总结一下,上文介绍了两种方式:
- 直接加大内存,使用:
node --max-old-space-size=4096
- 把一些耗内存进程独立出去, 使用了一个插件:
fork-ts-checker-webpack-plugin
希望大家留个印象, 记得这两种方式。
好了, 内容就这么多, 谢谢。
才疏学浅,如有错误, 欢迎指正。
谢谢。
原文:https://segmentfault.com/a/1190000039877970
前端常用图片文件下载上传方法
本文整理了前端常用的下载文件以及上传文件的方法
例子均以vue
+element ui
+axios
为例,不使用el
封装好的上传组件,这里自行进行封装实现
先附上demo
上传文件
以图片为例,文件上传可以省略预览图片功能
图片上传可以使用2种方式:文件流
和base64
;
1.文件流上传
+预览
:
<input type="file" id='imgBlob' @change='changeImgBlob' />
<el-image style="width: 100px; height: 100px" :src="imgBlobSrc"></el-image>
// data
imgBlobSrc: ""
// methods
changeImgBlob() {
let file = document.querySelector("#imgBlob");
/**
*图片预览
*更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
*/
var ua = navigator.userAgent.toLowerCase();
if (/msie/.test(ua)) {
this.imgBlobSrc = file.value;
} else {
this.imgBlobSrc = window.URL.createObjectURL(file.files[0]);
}
//上传后台
const fd = new FormData();
fd.append("files", file.files[0]);
fd.append("xxxx", 11111); //其他字段,根据实际情况来
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
headers: { "Content-Type": "multipart/form-data" },
data: fd
});
}
2.Base64上传
+预览
:
<input type="file" id='imgBase' @change='changeImgBase' />
<el-image style="width: 100px; height: 100px" :src="imgBaseSrc"></el-image>
// data
imgBaseSrc : ""
// methods
changeImgBase() {
let that = this;
let file = document.querySelector("#imgBase");
/**
*图片预览
*更适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
*/
if (window.FileReader) {
var fr = new FileReader();
fr.onloadend = function (e) {
that.imgBaseSrc = e.target.result;
//上传后台
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
data: {
files: that.imgBaseSrc
}
});
};
fr.readAsDataURL(file.files[0]);
}
}
下载文件
图片下载
假设需要下载图片为url
,文件流
处理和这个一样
<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
- 注意:这里需要指定
responseType
为blob
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}
文件下载(以pdf为例)
<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}
pdf预览可以参考如何预览以及下载pdf文件
原文:https://segmentfault.com/a/1190000039893814
web 埋点实现原理了解一下
前言
埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。
现有埋点三大类型
用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
- 手动埋点
手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。 - 可视化埋点
可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。 - 无埋点
无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。
我们暂时放弃可视化埋点的实现,在 手动埋点
和 无埋点
上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。
思考几个问题
埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
- 我们要采集什么内容,进行哪些采集接口的约定
- 业务方通过什么方式来调用我们的采集脚本
- 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
- 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
- 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
- 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
- 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
- 混合应用:app 与 h5 的混合应用我们要怎么进行通讯
我们要采集什么内容,进行哪些采集接口的约定
第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定
{
"header":{ // HTTP 头部
"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
},
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
"CurrentTime": “1517798922201”, //事件发生的时间
"ExtraInfo": {
} //扩展字段,对具体业务分析的传参
}]
}
以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。
{
"header":{ // HTTP 头部
"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
},
"body":{ // HTTP Body体
"DeviceType": "web" , //设备类型
"ScreenWide" : 768 , // 屏幕宽
"ScreenHigh": 1366 , // 屏幕高
"Language": "zh-cn" //语言
}
}
手动埋点:SDK
如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集
//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})
游客与用户关联
我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联
web 设备Id
用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹
我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)
iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}
//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId
if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
},1000)
}
})
}
iframe 与 SDK 通讯
function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data = {
deviceId: _deviceId,
type:event.data
}
event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}
//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
}else{
window.attachEvent("onmessage", receiveMessageFromIndex, false)
如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享
单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转
的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。
window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了
// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法
collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法
(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);
这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理
混合应用:app 与 h5 的混合应用我们要怎么进行通讯
现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。
纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器
// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {
collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
} else {
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
}
},1000)
}
实现思路
通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。
我们需要暴露给业务方调用的方法
我们来看下几个核心代码的实现
工具方法
我们定义了几个工具方法,提高开发的幸福指数 😝
var helper = {};
// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}
// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}
//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}
//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}
//将相对路径解析成文档全路径
helper.normalize = function(){}
采集逻辑
var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
device:{}
};
//获取埋点配置
collect.setParames = function(){}
//更新访问路径及页面信息
collect.updatePageInfo = function(){}
//获取事件参数
collect.getParames = function(){}
//获取设备信息
collect.getDevice = function(){}
//事件采集
collect.send = function(){}
//设备采集
collect.sendDevice = function(){}
//判断才否采集,埋点采集的开关
collect.isupload = function(){
1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
}
//点击事件处理函数
collect.clickHandler = function(){}
//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}
//页面回退事件处理函数
collect.onPopStateHandler = function(){}
//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}
//获取记录开始加载数据信息
collect.getBeforeload = function(){}
//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){
1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
}
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}
//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}
//采集自定义事件类型
collect.dispatch = function(){}
//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}
//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}
//页面初始化调用方法
collect.init = function(){
1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法
}
collect.init(); // 初始化
//暴露给业务方调用的方法
return {
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
}
原文链接:https://segmentfault.com/a/1190000014922668
简易版 React-Router实现
上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router
设计思路
由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两种pushstate 和 浏览器的前进和回退。刷新页面还是处于当前的URL,不涉及URL的改变。上一篇文章中也讲到 前端路由的原理有两点
- URL改变 页面不刷新。
- 监听到URL的改变。
所以在设计 react-router 的时候需要考虑 pushstate 和 浏览器的前进和回退这两种方式的URL改变。
Router
功能:负责监听页面对象发生了改变,并开始重新渲染页面 **
- 先定义一个上下文,方便把history数据传入所有的子组件
const RouteContext = React.createContext({})
- 定义 Router 组件,主要内容监听URL变化
const globalHistory = window.history // history 使用window 全局的history
class Router extends React.Component {
constructor(props) {
super(props)
this.state = { // 把location 设置为state 每次URL的改变,能够更新页面
location: window.location
}
// 第一种跳转方式:浏览器的前进后退,触发popstate 事件
window.addEventListener("popstate", () => {
this.setState({
location: window.location
})
})
}
// 第二种跳转方式:pushstate
// 向子组件提供push 方法更新路由,跳转页面
push = (route) => {
globalHistory.pushState({}, "", route)
this.setState({
location: window.location
})
}
// 定义上下文,把通用内容传入子组件
render() {
const { children } = this.props
const { location } = this.state
return (
<RouteContext.Provider value={{
history: globalHistory,
location,
push: this.push,
}}>
{
React.cloneElement(children, {
history: globalHistory,
location,
push: this.push,
})
}
</RouteContext.Provider>
)
}
}
export default Router
Route
功能:页面开始渲染后,根据具体的页面location信息展示具体路由地址对应的内容 **
import React, { useContext } from 'react'
const Route = (props) => {
// 在上下文中获取到相关信息
const context = useContext(RouteContext)
// 计算 location 匹配到的 path
const computedPath = (path, exact) => {
...TODO
// 这里内容和源码一样,其核心使用了path-to-regexp 库,能够计算出URL中的参数
}
// eslint-disable-next-line no-unused-vars
const { render, children, component, path, exact = false, ...rest } = props
const match = computedPath(path, exact)
const params = { ...context, match, location: context.location }
// 渲染 也就是源码中的三目运算。把相关的属性传入子组件
if (match) {
if (children) {
if (typeof children === 'function') {
return children(params)
}
return React.cloneElement(children, params)
} else if (component) {
return component(params)
} else if (render) {
return render(params)
}
}
return null
}
export default Route
这样一个简单的React-Router 就实现了,能够实现页面的跳转。
完整代码:https://github.com/LiuSandy/web
原文链接:https://zhuanlan.zhihu.com/p/366482879
React setState数据更新机制
为什么使用setState
在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个demo。
class Index extends React.Component {
this.state = {
count: 0
}
onClick = () => {
this.setState({
count: 10
})
}
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.onClick}>click</button>
</div>
)
}
}
根据上面代码可以看到,点击按钮后把state 中 count 的值修改为 10。并更新页面的显示。所以state的改变有两个作用:对应的值改变 和 页面更新。要想做到这两点在react 中 非 setState 不可。 假如说我们把 onClick 的方法内容修改为 this.state.count = 10
并在方法内打印出 this.state
的值,可以看到state的值已经改变。但是页面并没有更新到最新的值。 ☆总结一下:
- state 值的改变,目的是页面的更新,希望React 使用最新的 state来渲染页面。但是直接赋值的方式并不能让React监听到state的变化。
- 必须通过setState 方法来告诉React state的数据已经变化。
☆扩展一下:
在vue中,采用的就是直接赋值的方式来更新data 数据,并且Vue也能够使用最新的data数据渲染页面。这是为什么呢? 在vue2中采用的是Object.defineProperty()
方式监听数据的get 和 set 方法,做到数据变化的监听 在vue3中采用的是ES6 的proxy
方式监听数据的变化
setState 的用法
想必所有人都会知道setState 的用法,在这里还是想记录一下: setState方法有两个参数:第一个参数可以是对象直接修改属性值,也可以是函数能够拿到上一次的state值。第二个参数是一个可选的回调函数,可以获取最新的state值 回调函数会在组件更新完成之后执行,等价于在 componentDidUpdate
生命周期内执行。
- 第一个参数是对象时:如同上文的demo一样,直接修改state的属性值
this.setState({
key:newState
})
- 第一个参数是函数时:在函数内可以获取上一次state 的属性值。
// prevState 是上一次的 state,props 是此次更新被应用时的 props
this.setState((prevState, props) => {
return {
key: prevState.key
}
})
他们两者的区别主要体现在setState的异步更新上面!!!
异步更新还是同步更新
setState()
将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式 将setState()
视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。
先修改一下上面的代码,如果在onClick 方法中连续调用三次setState,根据上文可知 setState是一个异步的方式,每次调用只是将更改加入队列,同步调用的时候只会执行最后一次更新,所以结果是1而不是3。
onClick = () => {
const { count } = this.state
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
}
可以把上面代码理解为 Object.assign()
方法,
Object.assign(
state,
{ count: state.count + 1 },
{ count: state.count + 1 },
{ count: state.count + 1 }
)
如果第一个参数传入一个函数,连续调用三次,是不是和传入对象方式的结果是一样的呢?
onClick = () => {
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}
结果和传入对象的方式大相径庭,使用函数的方式就能够实现自增为3的效果。这又是为什么呢? 在函数内能够拿到最新的state 和 props值。由上文可知 setState 的更新是分批次的,使用函数的方式确保了当前state 是建立在上一个state 之上的,所以实现了自增3的效果。
☆总结一下: 为什么setState 方法是异步的呢?
- 可以显著的提升性能,react16 引入了 Fiber 架构,Fiber 中对任务进行了划分和优先级的分类,优先处理优先级比较高的任务。页面的响应就是一个优先级比较高任务,所以如果setState是同步,那么更新一次就要更新一次页面,就会阻塞到页面的响应。最好的办法就是获得到多个更新,之后进行批量的更新。只更新一次页面。
- 如果同步更新state,但是还没有执行render 函数,那么state 和 props 就不能够保持同步。
是不是所有的setState 都是异步的形式呢?答案是 否!!!在React 中也会存在setState 同步的场景
onClick = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
}, 0)
}
上面的代码会打印出0,2。这又是为什么呢?其实React 中的 setState 并不是严格意义上的异步函数。他是通过队列的延迟执行实现的。使用 isBatchingUpdates
判断当前的setState 是加入到更新队列还是更新页面。当 isBatchingUpdates=ture
是加入更新队列,否则执行更新。
知道了React 是使用 isBatchingUpdates
来判断是否加入更新队列。那么为什么在 setTimeout
事件中 isBatchingUpdates
值为 false
? 原因就是在React中,对HTML的原生事件做了一次封装叫做合成事件。所以在React自己的生命周期和合成事件中,可以控制 isBatchingUdates
的值,可以根据值来判断是否更新页面。而在宿主环境提供的原生事件中(即非合成事件),无法将 isBatchingUpdates
的值置为 false,所以就会立即执行更新。
☆所以setState 并不是有同步的场景,而是在特殊的场景下不受React 的控制 **
总结
setState 并不是单纯的同步函数或者异步函数,他的同步和异步的表现差异体现在调用的场景不同。在React 的生命周期和合成事件中他表现为异步函数。而在DOM的原生事件等非合成事件中表现为同步函数。
本节通过分析setState 的更新机制了解到setState 同步和异步的两种场景,下一节深入剖析下调用setState都做了什么?结合源码了解下为什么会出现两种场景?
原文:https://zhuanlan.zhihu.com/p/366781311
收起阅读 »配置 ESLint 自动格式化自闭合标签(Self closing tag)
对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,
- <SomeComponent></SomeComponent>
+ <SomeComponent/>
通过配置 ESLint 可在格式化的时候将标签自动变成自闭合形式。
create-react-app
如果是使用 create-react-app
创建的项目,直接在 package.json 的 eslint 配置部分加上如下配置即可:
"eslintConfig": {
"extends": "react-app",
+ "rules": {
+ "react/self-closing-comp": [
+ "error"
+ ]
}
安装依赖
安装 ESLint 相关依赖:
$ yarn add eslint eslint-plugin-react
如果是 TypeScript 项目,还需要安装如下插件:
$ yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser
配置 ESLint
通过 yarn eslint --init
向导来完成创建,
或手动创建 .eslintrc.json
填入如下配置:
{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint"],
"rules": {
"react/self-closing-comp": ["error"]
}
}
安装 ESLint for Vscode
当然了,还需要安装 VSCode 插件 dbaeumer.vscode-eslint。
然后配置 VSCode 在保存时自动进行修正动作:
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
使用
完成上述配置后,如果发现保存时,格式并未生效,或者只 JavaScript 文件生效,需要补上如下的 VSCode 配置:
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
]
也可查看 VSCode 的状态栏,看是否有报错可确定是什么原因导致 ESLint 工作不正常,比如 mac BigSur 中细化了权限,需要点击警告图标然后点击允许。
原文:https://zhuanlan.zhihu.com/p/368639332
收起阅读 »浅谈前端权限设计方案
前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可.
比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制,他也只被允许看到页面的部分内容.
出于实际工作的需要,很多项目(尤其类后台管理系统)需要引入权限控制.倘若权限整体的架构设计的不好或者没有设计,会导致项目中各种权限代码混入业务代码造成结构混乱,其次想给新模块引入权限控制或者功能扩展都十分棘手.
虽然前端在权限层面能做一些事情,但很遗憾真正对权限进行把关的是后端.例如一个软件系统,前端在不写一行权限代码的情况下,当用户进入某个他无权访问的页面时,后端是可以判断他越权访问并拒绝返回数据的.由此可见前端即使不做什么整个系统也是可以正常运行的,但这样应用的体验很不好.另外一个很重要的原因就是前端做的权限校验都是可以被本地数据造假越权通过.
前端如果能判断某用户越权访问页面时,就不要让他进入那张页面后再弹出无权访问的信息提示,因为这样体验很差.最优方案是直接关闭那些页面的入口,只让他看到他能访问的页面.即使他通过输入路径恶意访问,导航最后只会将它带到默认页面或404
页面.
前端做的权限控制大抵是先接受后台发送的权限数据,然后将数据注入到应用中.整个应用于是开始对页面的展现内容以及导航逻辑进行控制,从而达到权限控制的目的.前端做的权限控制虽然能提供一层防护,但根本目的还是为了优化体验.
本文接下来将从下面三个层面,从易到难步步推进,讲述目前前端主流的权限控制方案的实现.(下面代码将会以vue3
和vue-router 4
演示)
- 登录权限控制
- 页面权限控制
- 内容权限控制
登录权限控制要做的事情,是实现哪些页面能被游客访问,哪些页面只有登录后才能被访问.在一些没有引入角色的软件系统中,通过是否登录来评定页面能否被访问在实际工作中非常常见.
实现这个功能也非常简单,首先按照惯例定义一份路由.
export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/list", // 列表页
name:"List",
component: List,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
}
]
假定存在三个页面:登录页、列表页和个人中心页.登录页和列表页所有人都可以访问,但个人中心页面需要登录后才能看到,给该路由添加一个meta
对象,并将need_login
置为true
;
另外对于那些需要登录后才能看到的页面,用户如果没有登录就访问,就将页面跳转到登录页.等到他填写完用户名和密码点击登录后直接跳转到原来他想访问的页面.
在代码层面,通过router.beforeEach
可以轻松实现上述目标,每次页面跳转时都会调用router.beforeEach
包裹的函数,代码如下.
to
是要即将访问的路由信息,从其中拿到need_login
的值可以判断是否需要登录.再从vuex
中拿到用户的登录信息.
如果用户没有登录并且要访问的页面又需要登录时就使用next
跳转到登录页面,并将需要访问的页面路由名称通过redirect_page
传递过去,在登录页面就可以拿到redirect_page
等登录成功后直接跳转.
//vue-router4 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach((to, from, next) => {
const { need_login = false } = to.meta;
const { user_info } = store.state; //从vuex中获取用户的登录信息
if (need_login && !user_info) {
// 如果页面需要登录但用户没有登录跳到登录页面
const next_page = to.name; // 配置路由时,每一条路由都要给name赋值
next({
name: 'Login',
params: {
redirect_page: next_page,
...from.params, //如果跳转需要携带参数就把参数也传递过去
},
});
} else {
//不需要登录直接放行
next();
}
});
页面权限控制
页面权限控制要探讨的问题是如何给不同角色赋予不同的页面访问权限,接下来先了解一下角色的概念.
在一些权限设置比较简单的系统里,使用上面第一种方法就足够了,但如果系统引入了角色,那么就要在上面基础上,再进一步改造增强权限控制的能力.
角色的出现是为了更加个性化配置权限列表.比如当前系统设置三个角色:普通会员,管理员以及超级管理员.普通会员能够浏览软件系统的所有内容,但是它不能编辑和删除内容.管理员拥有普通会员的所有能力,另外它还能删除和编辑内容.超级管理员拥有软件系统所有权限,他单独拥有赋予某个账号为管理员或移除其身份的能力.
一旦软件系统引入了角色的概念,那么每个账户在注册之后就会被赋予相应的角色,从而拥有相应的权限.我们前端要做的事情就是依据不同角色给与它相应页面访问和操作的权限.这里要注意,前端依据的客体是角色,不是某个账户,因为账户是依托于角色的.
普通会员,管理员以及超级管理员这样角色的安排还是一种非常简单的划分方式,在实际项目中,角色的划份要更加细致的多.比如一些常见的后台业务系统,软件系统会按照公司的各个部门来建立角色,诸如市场部,销售部,研发部之类.公司的每个成员就会被划分到相应角色中,从而只具备该角色所拥有的权限.
公司另外一些高层领导他们的账户则会被划分到普通管理员或高级管理员中,那么他们相较于其他角色也会拥有更多的权限.
上面介绍那么多角色的概念其实是为了从全栈的维度去理解权限的设计,但真正落地到前端项目中是不需要去处理角色逻辑的,那部分功能主要由后端完成.
现在假定后端不处理角色完全交给前端来做会出现什么问题.首先前端新建一个配置文件,假定当前系统设定三种角色:普通会员,管理员以及超级管理员以及每个角色能访问的页面列表(伪代码如下).
export const permission_list = {
member:["List","Detail"], //普通会员
admin:["List","Detail","Manage"], // 管理员
super_admin:["List","Detail","Manage","Admin"] // 超级管理员
}
数组里每个值对应着前端路由配置的name
值.普通会员能访问列表页
和详情页
,管理员能额外访问内容管理页面
,超级管理员能额外访问人员管理页面
.
整个运作流程简述如下.当用户登录成功之后,通过接口返回值得知用户数据和所属角色.拿到角色值后就去配置文件里取出该角色能访问的页面列表数组,随后将这部分权限数据加载到应用中从而达到权限控制的目的.
从上面流程看,角色放在前端配置也是可以的.但假如项目已经上线,产品经理要求项目急需增加一个新角色合作伙伴
,并把原来已经存在的用户张三
移动到合作伙伴
角色下面.那这样的变动会导致前端需要修改代码文件,在原来的配置文件上再新建角色来满足这一需求.
由此可见由前端来配置角色列表是非常不灵活且容易出错的,那么最优方案是交给后端去配置.用户一旦登录后,后端接口直接返回该账号拥有的权限列表就行了,至于该账户属于什么角色以及角色拥有的页面权限全部丢给后端去处理.
用户登录成功后,后端接口数据返回如下.
{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}
前端现在不需要理会张三属于什么角色,只需要按照张三的权限列表给他相应的访问权限就行了,其他都交给后端处理.
通过接口的返回值permission_list
可知,张三能访问列表页
、详情页
以及内容管理页
.我们先回到路由配置页面,看看如何配置.
//静态路由
export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
},
{
path:"/", // 首页
name:"Home",
component: Home,
}
]
//动态路由
export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
},
{
path:"/detail", // 详情页
name:"Detail",
component: Detail
},
{
path:"/manage", // 内容管理页
name:"Manage",
component: Manage
},
{
path:"/admin", // 人员管理页
name:"Admin",
component: Admin
}
]
现在将所有路由分成两部分,静态路由routes
和动态路由dynamic_routes
.静态路由里面的页面是所有角色都能访问的,它里面主要区分登录访问和非登录访问,处理的逻辑与上面介绍的登录权限控制
一致.
动态路由dynamic_routes
里面存放的是与角色定制化相关的的页面.现在继续看下面张三的接口数据,该如何给他设置权限.
{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}
用户登录成功后,一般会将上述接口信息存到vuex
和localStorage
里面.假如此时刷新浏览器,我们就要动态添加路由信息.
import store from "@/store";
export const routes = [...]; //静态路由
export const dynamic_routes = [...]; //动态路由
const router = createRouter({ //创建路由对象
history: createWebHashHistory(),
routes,
});
//动态添加路由
if(store.state.user != null){ //从vuex中拿到用户信息
//用户已经登录
const { permission_list } = store.state.user; // 从用户信息中获取权限列表
const allow_routes = dynamic_routes.filter((route)=>{ //过滤允许访问的路由
return permission_list.includes(route.name);
})
allow_routes.forEach((route)=>{ // 将允许访问的路由动态添加到路由栈中
router.addRoute(route);
})
}
export default router;
核心代码在动态添加路由里面,主要利用了vue-router 4
提供的API
即router.addRoute
,它能够给已经创建的路由实例继续添加路由信息.
我们先从vuex
里面拿到当前用户的权限列表,然后遍历动态路由数组dynamic_routes
,从里面过滤出允许访问的路由,最后将这些路由动态添加到路由实例里.
这样就实现了用户只能按照他对应的权限列表里的规则访问到相应的页面,至于那些他无权访问的页面,路由实例根本没有添加相应的路由信息,因此即使用户在浏览器强行输入路径越权访问也是访问不到的.
由于vue-router 4
废除了之前的router.addRoutes
,换成了router.addRoute
.每一次只能一个个添加路由信息,所以要将allow_routes
遍历循环添加.
动态添加路由这部分代码最好单独封装起来,因为用户第一次使用还没登录时,store.state.user
是为空的,上面动态添加路由的逻辑会被跳过.那么在用户登录成功获取到权限列表的信息后,需要再把上面动态添加路由的逻辑执行一遍.
添加嵌套子路由
假如静态路由的形式如下,现在想把列表页添加到Tabs
嵌套路由的children
里面.
const routes = [
{
path: '/', //标签容器
name: 'Tabs',
component: Tabs,
children: [{
path: '', //首页
name: 'Home',
component: Home,
}]
}
]
export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
}
]
官方router.addRoute
给出了相应的配置去满足这样的需求(代码如下).router.addRoute
接受两个参数,第一个参数对应父路由的name
属性,第二个参数是要添加的子路由信息.
router.addRoute("Tabs", {
path: "/list",
name: "List",
component: List,
});
切换用户信息是非常常见的功能,但是应用在切换成不同账号后可能会引发一些问题.例如用户先使用超级管理员登录,由于超级管理员能访问所有页面,因此所有页面路由信息都会被添加到路由实例里.
此时该用户退出账号,使用一个普通会员的账号登录.在不刷新浏览器的情况下,路由实例里面仍然存放了所有页面的路由信息,即使当前账号只是一个普通会员,如果他越权访问相关页面,路由还是会跳转的,这样的结果并不是我们想要的.
解决方案有两个.第一是用户每次切换账户后刷新浏览器重新加载,刷新后的路由实例是重新配置的所以可以避免这个问题,但是刷新页面会带来不好的体验.
第二个方案是当用户选择登出后,清除掉路由实例里面处存放的路由栈信息(代码如下).
const router = useRouter(); // 获取路由实例
const logOut = () => { //登出函数
//将整个路由栈清空
const old_routes = router.getRoutes();//获取所有路由信息
old_routes.forEach((item) => {
const name = item.name;//获取路由名词
router.removeRoute(name); //移除路由
});
//生成新的路由栈
routes.forEach((route) => {
router.addRoute(route);
});
router.push({ name: "Login" }); //跳转到登录页面
};
移除单个路由主要利用了官方提供的API
,即router.removeRoute
.
路由栈清空后什么页面都不能访问了,甚至登录页面都访问不了.所以需要再把静态的路由列表routes
引入进来,使用router.addRoute
再添加进入.这样就能让路由栈恢复到最初的状态.
内容权限控制
页面权限控制
它能做到让不同角色访问不同的页面,但对于一些颗粒度更小的项目,比如希望不同的角色都能进入页面,但要求看到的页面内容不一样,这就需要对内容进行权限控制了.
假设某个后台业务系统的界面如下图所示.表格里面存放的是列表数据,当点击发布需求时跳转到新增页面.当勾选列表中的某一条数据后,点击修改按钮显示修改该条数据的弹出框.同理点击删除按钮显示删除该条数据的弹出框

假设项目需求该系统存在三个角色:职员、领导和高层领导.职员不具备修改
、删除
以及发布需求
的功能,他只能查看列表
.当职员进入该页面时,页面上只显示列表
内容,其他三个按钮移除.
领导角色保留列表
和发布需求
按钮.高级领导角色保留页面上所有内容.
我们拿到图片后要先要对页面内容整体分析一遍,按照增删查改
四个维度对页面内容进行归类.使用简称CURD
来标识(CURD
分别代表创建(Create
)、更新(Update
)、读取(Retrieve
)和删除(Delete
)).
上图中列表
内容属于查询操作,因此设定为R
.凡是具备R
权限的用户就显示该列表
内容.
发布需求
属于新增操作,设定凡是具备C
权限的用户就显示该按钮.
同理修改
按钮对应着U
权限,删除
按钮对应着D
权限.
由此可以推断出职员角色在该页面的权限编码为R
,它只能查看列表
内容无法操作.
领导角色对应的权限编码为CR
.高级领导对应的权限编码为CURD
.
现在用户登录完成后,假设后端接口返回的数据如下(将这条数据存到vuex
):
{
user_id:1,
user_name:"张三",
permission_list:{
"List":"CR", //权限编码
"Detail":"CURD" //权限编码
}
}
张三除了静态路由设置的页面外,他只能额外访问List
列表页以及Detail
详情页.其中列表页他只具备创建和新增权限,详情页他具备增删查改所有权限.那么当张三访问上图中的页面时,页面中应该只显示列表
和发布需求
按钮.
我们现在要做的就是设计一个方案尽可能让页面内容方便被权限编码控制.首先创建一个全局的自定义指令permission
,代码如下:
import router from './router';
import store from './store';
const app = createApp(App); //创建vue的根实例
app.directive('permission', {
mounted(el, binding, vnode) {
const permission = binding.value; // 获取权限值
const page_name = router.currentRoute.value.name; // 获取当前路由名称
const have_permissions = store.state.permission_list[page_name] || ''; // 当前用户拥有的权限
if (!have_permissions.includes(permission)) {
el.parentElement.removeChild(el); //不拥有该权限移除dom元素
}
},
});
当元素挂载完毕后,通过binding.value
获取该元素要求的权限编码.然后拿到当前路由名称,通过路由名称可以在vuex
中获取到该用户在该页面所拥有的权限编码.如果该用户不具备访问该元素的权限,就把元素dom
移除.
对应到上面的案例,在页面里按照如下方式使用v-permission
指令.
<template>
<div>
<button v-permission="'U'">修改</button> <button v-permission="'D'">删除</button>
</div>
<p>
<button v-permission="'C'">发布需求</button>
</p>
<!--列表页-->
<div v-permission="'R'">
...
</div>
</template>
将上面模板代码和自定义指令结合理解一下就很容易明白整个内容权限控制的逻辑.首先前端开发页面时要将页面分析一遍,把每一块内容按照权限编码分类.比如修改按钮就属于U
,删除按钮属于D
.并用v-permission
将分析结果填写上去.
当页面加载后,页面上定义的所有v-permission
指令就会运行起来.在自定义指令内部,它会从vuex
中取出该用户所拥有的权限编码,再与该元素所设定的编码结合起来判端是否拥有显示权限,权限不具备就移除元素.
虽然分析过程有点复杂,但是以后每个新页面想接入权限控制非常方便.只需要将新页面的各个dom
元素添加一个v-permission
和权限编码就完成了,剩下的工作都交给自定义指令内部去做.
延伸

如果项目中删除操作并不是单独放置在一个按钮,而是与列表捆绑在一起放在表格的最后一列,如下图所示.
这样的界面样式在实际工作中非常常见,但似乎上面的v-permission
就并不能友好的支持这样的样式.自定义指令在这种情况下虽然不能用,但我们仍然可以采用相同的思路去优化我们现有的代码结构.
例如模板代码如下.整个列表被封装成了一个组件List
,那么在List
内部就可以写很多的逻辑控制。
比如List
组件内也可以通过vuex
拿到该用户在当前页面的权限编码,如果发现具备D
权限就显示列表中最后删除
那一列,否则就不显示.至于整个列表的显示隐藏仍然可以使用v-permission
来控制.
<template>
<div>
<button v-permission="'C'">添加资源</button>
</div>
<!--列表页-->
<List v-permission="'R'">
...
</List>
</template>
动态导航
下图中的动态导航也是实际工作中非常常见的需求,比如销售部所有成员只能看到销售模块下的两个页面,同理采购部成员只能看到采购模块下的页面.
下面侧边栏导航组件需要根据不同权限显示不同的页面结构,以满足不同角色群体的要求.
我们要把这种需要个性化设置的组件与上面使用v-permission
控制的模式区分开.上面那些页面之所以能使用v-permission
来控制,主要原因是因为产品经理在设计整个软件系统的页面时是按照增删查改
的规则进行的.因此我们就能抽象出其中存在的共性与规律,再借助自定义指令来简化权限系统的开发.
但是侧边栏组件一般全局只有一个,没有什么特别的规律而言,那就只需要在组件内部使用v-if
依据权限值动态渲染就行了.
比如后台接口如下:
{
user_id:1,
user_name:"张三",
permission_list:{
"SALE":true, //显示销售大类
"S_NEED":"CR", //权限编码
"S_RESOURCE":"CURD", //权限编码
}
}
张三拥有访问需求
和资源
页面,但注意SALE
并没有与哪个页面对应上,它仅仅只是表示是否显示销售
这个一级导航.
接下来在侧面栏组件通过vuex
拿到权限数据,再动态渲染页面就可以了.
<template>
<div v-if="permission_list['HOME']">系统首页</div>
<div v-if="permission_list['SALE']">
<p>销售</p>
<div v-if="permission_list['S_NEED']">需求</div>
<div v-if="permission_list['S_RESOURCE']">资源</div>
</div>
<div v-if="permission_list['PURCHASE']">
<p>采购</p>
<div v-if="permission_list['P_NEED']">需求</div>
<div v-if="permission_list['P_RESOURCE']">资源</div>
</div>
</template>
尾言
前端提供的权限控制为应用加固了一层保险,但同时也要警惕前端设定的校验都是可以通过技术手段破解的.权限问题关乎到软件系统所有数据的安危,重要性不言而喻.
为了确保系统平稳运行,前后端都应该做好自己的权限防护.
原文链接:https://juejin.cn/post/6949453195987025927
JavaScript 对象
为什么要有对象?
- 如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据
什么是对象?
现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征
举例:一部车,一个手机
车是一类事物。,门口停的那辆车才是对象
特征:红色、四个轮子
行为:驾驶、刹车
- JavaScript 中的对象:
- JavaScript 中的对象其实就是生活中对象的一个抽象
- JavaScript 的对象是无序属性的集合
- 其属性可以包含基本值、对象或函数。对象就是一组没有顺序的值。我们可以把 JavaScript 中的对象想象成键值对,其中值可以是数据和函数
- 对象的行为和特征:
- 特征---在对象中用属性表示
- 行为---在对象中用方法表示
对象字面量(用字面量创建对象)
- 创建一个对象最简单的方法是使用对象字面量赋值给变量。类似数组
- 对象字面量语法:{}
- 内部可以存放多条数据,数据与数据之间用逗号分隔,最后一个后面不要加逗号
- 每条数据都是有属性名和属性值组成,键值对写法: k : v
- k:属性名
- v:属性值,可以实任意类型的数据,比如简单类型数据、函数、对象
var obj = {
k:v,
k:v,
k:v,
};
区分属性和方法
- 属性:对象的描述性特征,一般是名词,相当于定义在对象内部的变量
- 方法:对象的行为和功能,一般是动词,定义在对象中的函数
调用对象内部属性和方法的语法
- 用对象的变量名打点调用某个属性名,得到属性值
- 在对象内部用 this 打点调用属性名。this 替代对象
- 用对象的变量名后面加 [] 调用,[] 内部是字符串格式的属性名
- 调用方法时,需要在方法名后加 () 执行
/*
现实生活中:万物皆对象 对象是一个具体事物 看得见摸得着的实物
对象是一组无序的相关属性和方法的集合 所有事物的是对象
对象是由属性和方法组成的
属性:事物的特征 在对象中用属性来表示(常用名词)
方法:事物的行为 在对象中用方法来表示(常用动词)
对象的字面量:就是花括号{} 里面包含了表达这个具体实物(对象)的属性和方法
*/
//创建一个空的对象
var obj = {
uname:'张三',
age:'男',
sayHi: function () {
console.log('Hi!');
console.log(this.uname + "向你说您好");
}
}
// 1.我们在创建对象时我们采用键值对的形式 键 属性名 : 属性 属性值
// 2.多个属性或者方法中间用逗号隔开
// 3.方法冒号后面跟的是一个匿名函数
// 使用对象
// 1)调用对象的属性 我们采取 对象名.属性名
console.log(obj.uname);
// 2)调用对象也可以 对象名['属性名']
console.log(obj['age']);
// 3)调用对象的方法 对象.方法名
obj.sayHi();
更改对象内部属性和方法的语法
- 更改属性的属性值方法:先调用属性,再等号赋值
obj.age = 19;
- 增加新的属性和属性值:使用点语法或者 [] 方法直接定义新属性,等号赋值
obj.height = 180;
- 删除一条属性:使用一个 delete 关键字,空格后面加属性调用
delete obj.sex;
new Object() 创建对象
- object() 构造函数,是一种特殊的函数。主要用来再创建对象时初始化对象,即为对象成员变量赋值初始值,总与 new 运算符一起使用在创建对象的语句中
- 构造函数用于创建一类对象,首字母要大写
- 构造函数要和 new 一起使用才有意义
// 利用new object 创建对象
var obj = new Object();//创建了一个空对象
obj.name = '张三';
obj.age = 18;
obj.sex = '男';
obj.sayHi = function() {
console.log('Hi~');
}
//1.我们是利用等号赋值的方法给对象 属性和方法 赋值
//2.每个 属性和方法 用分号结束
// 调用
console.log(obj.name);
console.log(obj['sex']);
obj.sayHi();
new 在执行时会做四件事情
- new 会在内存中创建一个新的空对象
- new 会让 this 指向这个新的对象
- 执行构造函数 目的 :给这个新对象加属性和方法
- new 会返回这个新的对象
工厂 函数创建对象
- 如果要创建多个类似的对象,可以将 new Object() 过程封装到一个函数中,将来调用函数就能创建一个对象,相当于一个生产对象的函数工厂,用来简化代码
// 我们为什么需要使用函数
// 就是因我们前面两种创建对象的方式一次只能创建一次对象
var ldh = {
uname: '刘德华',
age: 55,
sing = function() {
console.log('冰雨');
}
}
var zxy = {
uname: '张学友',
age: 58,
sing = function() {
console.log('李香兰');
}
}
// 因为我们一次创建一个对象 里面有很多的属性和方法是大量相同的 我们只能复制
// 因此我们可以利用函数的方法 重复这些相同的代码
// 又因为这个函数不一样 里面封装的不是普通代码 而是对象
// 函数 可以把我们对象里面一些相同的属性和方法抽象出来封装到函数里面
用 工厂方法 函数创建对象
function createStar(uname, age, sex) {
//创建一个空对象
var Star = new Object();
//添加属性和方法,属性可以接受参数的值
Star.name = uname;
Star.age = age;
Star.sex = sex;
Star.sing = function(sang) {
console.log(sang);
}
//将对象做为函数的返回值
return Star;
}
var p1 = createStar("张三",18,"男");
自定义构造函数
- 比工厂方法更加简单
- 自定义一个创建具体对象的构造函数,函数内部不需要 new 一个构造函数的过程,直接使用 this 代替对象进行属性和方法的书写,也不需要 return 一个返回值
- 使用时,利用 new 关键字调用自定义的构造函数即可
- 注意:构造函数的函数名首字母需要大写,区别于其他普通函数名
// 利用构造函数创建对象
// 我们需要创建四大天王的对象 相同的属性: 名字 年龄 性别 相同的方法 : 唱歌
// 构造函数的语法格式
/*
function 构造函数名() {
this.属性 = 值;
this.方法 = fucntion() {}
}
// 调用构造函数
new 构造函数名();
*/
function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;
this.sing = function(sang) {
console.log(sang);
}
}
var ldh = new Star('刘德华', 18, '男');
console.log(typeof ldh);//object
console.log(ldh.name);
console.log(ldh.age);
console.log(ldh.sex);
ldh.sing('冰雨');
// 1.构造函数首字母必须大写
// 2.构造函数不需要return就能返回结果
// 3.调用函数返回的是一个对象
var zxy = new Star('张学友', 29, '男')
console.log(zxy);
// 4.我们调用构造函数必须使用new
对象遍历
- for in 循环也是循环的一种,专门用来遍历对象,内部会定义一个 k 变量,k 变量在每次循环时会从第一个开始接收属性名,一直接收到最后一个属性名,执行完后会跳出循环。
- 简单的循环遍历:输出每一项的属性名和属性值
//循环遍历输出每一项
for(var k in obj){
console.log(k + "项的属性值" + obj[k]);
}
案例:
//遍历对象
var obj = {
uname: '王二狗',
age: 18,
sex: '男'
}
console.log(obj.uname);
console.log(obj.age);
console.log(obj.sex);
//但是一个一个输出很累
// 因此我们引出 for...in...语句 --用于对数组或者对象的属性进行循环操作
/*
基本格式:
for (变量 in 对象) {
}
*/
for (k in obj) {
console.log(k); //k变量输出 得到的是属性名
console.log(obj[k]); //obj[k] 输出对象各属性的属性值 切记不要用obj.k 那样就变成输出 k 属性名的属性值了 ---!!!:k是变量不加''
}
简单类型和复杂类型的区别
- 我们已经学过简单类型数据和一些复杂类型的数据,现在来看一下他们之间的区别有哪些
- 基本类型又叫做值类型,复杂类型又叫做引用类型
- 值类型:简单数据类型,基本数据类型,在存储时,变量中存储的是值本身,因此叫做值类型
- 引用类型:复杂数据类型,在存储时,变量中存储的仅仅是地址(引用),因此叫做引用数据类型
堆和栈
- JavaScript 中没有堆和栈的概念,此处我们用堆和栈来讲解,目的是方便理解和方便以后的学习

- 堆栈空间分配区别
- 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值相等
- 堆(操作系统):存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,有垃圾回收机制回收
简单数据类型(基本类型)在内存中的存储
变量中如果存储的是简单类型的数据,那么变量中存储的是值本身,如果将变量赋值给另一个变量,是将内部的值赋值一份给了另一个变量,两个变量之间没有联系,一个变化,另一个不会同时变化

var a = 5;
var b = a; //将 a 内部存储的数据 5 复制了一份
a = 10;
console.log(a);
console.log(b);
// 因此 a 和 b 发生改变,都不会互相影响
复杂数据类型(引用类型)在内存中的存储
如果将复杂数据赋值给一个变量,复杂类型的数据会在内存中创建一个原型,而变量中存储的是指向对象的一个地址,如果将变量赋值给另一个变量,相当于将地址复制一份给了新的变量,两个变量的地址相同,指向的是同一个原型,不论通过哪个地址更改了原型,都是在原型上发生的更改,两个变量下次访问时,都会发生变化

// 复杂数据类型
var p1 = {
name: "zs",
age: 18,
sex: "male"
}
var p = p1; //p1 将内部存储的指向对象原型的地址复制给了 p
// 两个变量之间是一个联动的关系,一个变化,会引起另一个变化
p.name = "ls";
console.log(p);
console.log(p1);
// 数组和函数存储在变量中时,也是存储的地址
var arr = [1,2,3,4];
var arr2 =arr;
arr[4] = 5;
console.log(arr);
console.log(arr2);
内置对象
- JavaScript 包含:ECMA DOM BOM
- ECMAscript 包含:变量、数据、运算符、条件分支语句、循环语句、函数、数组、对象···
- JavaScript 的对象包含三种:自定义对象 内置对象 浏览器对象
- ECMAscript 的对象:自定义对象 内置对象
- 使用一个内置对象,只需要知道对象中有哪些成员,有什么功能,直接使用
- 需要参考一些说明手册 W3C / MDN
MDN
Mozilla 开发者网络(MDN) 提供有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和 万维网 HTML5 应用的API
- MDN:https://developer.mozilla.org/zh-CN/
- 比如:通过查询 MDN 学习 Math 对象的 random() 方法的使用
如何学习一个方法?
- 方法的功能
- 参数的意义和类型
- 返回值意义和类型
- demo 进行测试
Math 对象
- Math 对象它具有数学常数和函数的属性和方法,我们可以直接进行使用
- 根据数学相关的运算来找 Math 中的成员(求绝对值,取整)
演示:
Math.PI | 圆周率 |
Math.random() | 生成随机数 |
Math.floor()/Math.ceil() | 向下取整/向上取整 |
Math.round() | 取整,四舍五入 |
Math.abs() | 绝对值 |
Math.max()/Math.min() | 求最大和最小值 |
Math.sin()/Math.cos() | 正弦/余弦 |
Math.power()/Math.sqrt() | 求指数次幂/求平方根 |
Math.random()
如何求一个区间内的随机值
Math.random()*(max_num - min_num) + min_num
Math.max()/Math.min()
// Math数学对象 不是一个构造函数 所以我们不需要用new来调用 而是直接使用里面的属性和方法即可
console.log(Math.PI); //一个属性值 圆周率
console.log(Math.max(99, 199, 299)); //299
console.log(Math.max(-10, -20, -30)); //-10
console.log(Math.max(-10, -20, '加个字符串')); //NaN
console.log(Math.max()); //-Infinity
console.log(Math.min(99, 199, 299)); //99
console.log(Math.min()); //Infinity
创建数组对象的第二种方式
字面量方式
new Array() 构造函数方法
// 字面量方法
// var arr = [1,2,3];
// 数组也是对象,可以通过构造函数生存
//空数组
var arr = new Array();
//添加数据,可以传参数
var arr2 = new Array(1,2,3);
var arr3 = new Array("zs","ls","ww");
console.log(arr);
console.log(arr2);
console.log(arr3);
// 检测数组的数据类型
console.log(typeof(arr));//object
console.log(typeof(arr2));//object
console.log(typeof(arr3));//object
由于 object 数据类型的范围较大,所以我们需要一个更精确的检测数据类型的方法
- instanceof 检测某个实例是否时某个对象类型
var arr = [1,2,3];
var arr1 = new Array(1,2,3)
var a = {};
// 检测某个实例对象是否属于某个对象类型
console.log(arr instanceof Array);//true
console.log(arr1 instanceof Array);//true
console.log(a instanceof Array);//true
function fun () {
console.log(1);
}
console.log(fun instanceof Function);//true
数组对象的属性和方法
toString()
- toString() 把数组转换成字符串,逗号分隔每一项
// 字面量方法
var arr = [1,2,3,4];
// toString() 方法:可以转字符串
console.log(arr.toString());//1,2,3,4
数组常用方法
首尾数据操作:
- push() 在数组末尾添加一个或多个元素,并返回数组操作后的长度
// 字面量方法
var arr = [1,2,3,4];
// 首尾操作方法
// 尾推,参数是随意的,有一个或者多个
console.log(arr.push(5,6,7,8)); //8(数组长度)
console.log(arr);//[1,2,3,4,5,6,7,8]
console.log(arr.push([5,6,7,8])); //5(数组长度)
console.log(arr);//[1,2,3,4,Array(4)]
- pop() 删除数组最后一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];
//尾删,删除最后一项数据
// 不需要传参
console.log(arr.pop());//4(被删除的那一项数据)
console.log(arr);//[1,2,3]
- shift() 删除数组第一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];
// 首删,删除第一项数据,不需要传参
console.log(arr.shift());//1
console.log(arr);//[2,3,4]
- unshift() 在数组开头添加一个或多个元素,并返回数组的长度
// 字面量方法
var arr = [1,2,3,4];
// 首添,参数与 push 方法类似
console.log(arr.unshift(-1,0));//6
console.log(arr);//[-1,0,1,2,3,4]
案例:将数组的第一项移动到最后一项
// 字面量方法
var arr = [1,2,3,4];
// 将数组的第一项移动到最后一项
// 删除第一项
// 将删除的项到最后一项
arr.push(arr.shift());
console.log(arr);//[2,3,4,1]
arr.push(arr.shift());
console.log(arr);//[3,4,1,2]
arr.push(arr.shift());
console.log(arr);//[4,1,2,3]
arr.push(arr.shift());
console.log(arr);//[1,2,3,4]
数组常用方法
合并和拆分:
concat()
- 将两个数组合并成一个新的数组,原数组不受影响。参数位置可以是一个数组字面量、数组变量、零散的值
// 字面量方法
var arr = [1,2,3,4];
// 合并方法
// 参数:数组 数组的变量 零散的值
// 返回值:一个新的拼接后的数组
var arr1 = arr.concat([5,6,7]);
console.log(arr);//[1,2,3,4]
console.log(arr1);//[1,2,3,4,5,6,7]
slice(start,end)
- 从当前数组中截取一个新的数组,不影响原来的数组,返回一个新的数组,包含从 start 到end (不包括该元素)的元素
- 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];
// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取
删除,插入,替换:
splice(index,howmany,element1,element2,...)
用于插入、删除或替换数组的元素
index:删除元素的开始位置
howmany:删除元素的个数,可以是0
element1,element2:要替换的新数据
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];
// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取
位置方法:
indexOf() 查找数据在数组中最先出现的下标
lastndexOf() 查找数据在数组中最后一次出现的下标
注意:如果没有找到返回-1
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,4,5];
// 查找某个元素在数组中从前往后第一次 出现位置的下标
console.log(arr.indexOf(4));//3 (数字4的下标)
// 查找某个元素在数组中从前往后最后一次出现位置的下标
console.log(arr.lastIndexOf(4));//10
console.log(arr.lastIndexOf(11));//-1 (代表数组中不存在11这个数据)
排序方法:
倒序:reverse() 将数组完全颠倒,第一项变成最后一项,最后一项变成第一项
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];
// 数组倒序
console.log(arr.reverse());//[10,9,8,7,6,5,4,3,2,1]
从大到小排序:sort() 默认根据字符编码顺序,从大到小排序
如果想要根据数值大小进行排序,必须添加sort的比较函数参数
该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数具有两个参数 a 和 b,根据 a 和 b 的关系作为判断条件,返回值根据条件分为三个分支,整数、负数、0:
返回值是负数-1:a 排在 b 前面
返回值是整数1:a 排在 b 后面
返回值是0:a 和 b 的顺序保持不变
人为控制的是判断条件
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 排序,默认按照字符编码顺序来排序
arr.sort();
console.log(arr);//[1,10,2,20,3,30,4,5,6,7,8,9] (如果不添加函数)
// 添加一个比较函数
arr.sort(function(a,b) {
if (a > b) {
return -1;//表示 a 要排在 b 前面
} else if (a < b) {
return 1;//表示 a 要排在 b后面
} else {
return 0;;//表示 a 和 b 保持原样,不换位置
}
});
console.log(arr);//[30,20,10,9,8,7,6,5,4,3,2,1] (添加函数之后)
// 想要从小到大排序只要将函数 大于小于 号,反向即可
转字符串方法:将数组的所有元素连接到一个字符串中
join() 通过参数作为连字符将数组中的每一项用连字符连成一个完整的字符串
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 转字符串方法
var str = arr.join();
console.log(str);//1,2,3,4,5,6,7,8,9,10,20,30
var str = arr.join("*");
console.log(str);//1*2*3*4*5*6*7*8*9*10*20*30
var str = arr.join("");
console.log(str);//123456789102030
清空数组方法总结
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 方式1 推荐
arr = [];
// 方式2
arr.length = 0;
// 方式 3
arr.splice(0,arr.length);
基本包装类型
为了方便操作简单数据类型,JavaScript 还提供了特殊的简单类型对象:String 基本类型时没有方法的。
当调用 str.substring() 等方法的时候,先把 str 包装成 String 类型的临时对象,再调用 substring 方法,最后销毁临时对象
// 基本数据类型:没有属性值和方法
// 对象数据类型:有属性和方法
// 但是:字符串是可以调用一些属性和方法
var str = "这是一个字符串";
var str2 = str.slice(3,5);
console.log(str2);//个字
// 基本包装类型,基本类型的数据在进行一些特殊操作时,会暂时被包装成一个对象,结束后再被销毁
// 字符串也有一种根据构造函数创建方法
var str3 = new String("abcdef");
console.log(str);//这是一个字符串
console.log(str3);//Sring{"abcdef"}
// 模拟计算机的工作
var str4 = new String(str);
// 字符串临时被计算机包装成字符串对象
var str2 = str4.slice(3,5);
str4 = null;
字符串的特点
字符串是不可变的
// 定义一个字符串
var a = "abc";
a = "cde";
// 字符串是不可变的,当 a 被重新赋值时,原来的值 "abc" 依旧在电脑内存中
// 在 JavaScript 解释器 固定时间释放内存的时候可能会被处理掉
由于字符串的不可变,在大量拼接字符串的时候会有效率问题
由于每次拼接一个字符串就会开辟一个空间去存储字符串
// 大量拼接字符串也效率问题
var sum = "";
for(var i = 1; i <= 10000000; i++) {
sum += i;
}
console.log(sum);
测试一下我们发现,浏览器转了一会才显示出来
因此在我们以后,不要大量用字符串拼接的方法,以后我们会有更好的方法替代
字符串属性
长度属性:str.length
字符串长度指的是一个字符串中所有的字符总数
字符串方法
indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置
- 找到指定的字符串在原字符串中第一次出现的位置的下标。如果子字符串在原字符串中没有,返回值是 -1
concat() 方法用于连接两个或多个字符串
- 参数比较灵活,可以是字符串、或者字符串变量、多个字符串
- 生成的是一个新的字符串,原字符串不发生变化
split() 方法用于把一个字符串分割成字符串数组(和数组中的 join() 方法是对应的)
- 参数部分是割符,利用分割符将字符串分割成多个部分,多个部分作为数组的每一项组成数组
- 如果分割符是空字符串,相当于将每个字符拆分成数组中的每一项
// 定义一个字符串
var str = "这是一个字符串,abc, $%#";
// 长度属性
console.log(str.length);//18
// charAt() 返回指定的下标位置的字符
console.log(str.charAt(6));//串 (字符串对象是一种伪数组,所以需要从 0 开始数)
// indexOf() 返回子串在原始字符串中第一次出现位置的下标
console.log(str.indexOf("字"));//4
console.log(str.indexOf("字符串"));//4
console.log(str.indexOf("字 符串"));//-1
// concat() 字符串拼接
var str2 = str.concat("哈哈哈","普通");
console.log(str);//这是一个字符串,abc, $%#
console.log(str2);//这是一个字符串,abc, $%#哈哈哈普通
// split() 分割字符串成一个数组
var arr = str.split("")//一个一个字符分割
console.log(arr);
var arr = str.split(",")//按逗号进行分割
console.log(arr);
// 字符串内容倒置
var arr = str.split("")//一个一个字符分割
arr.reverse();
strn = arr.join("");
console.log(strn);
// 用连续打点方式化简
var arr = str.split("").reverse().join("")
console.log(arr);
toLowerCase() 把字符串转换为小写
toUpperCase() 把字符串转换为大写
- 将所有的英文字符转为大写或者小写
- 生成的是新的字符串,原字符串不发生变化
// 大小写转换
var str1 = str.toUpperCase();
console.log(str);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%#
var str2 = str1.toLowerCase();
console.log(str2);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%# --字符串本身不会发生改变
截取字符串的三种方法
slice() 方法可以提取字符串的某个部分,并以新的字符串返回被提取的部分
- 语气:slice(start,end)
- 从开始位置截取到结束位置(不包括结束位置)的字符串
- 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾
substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符
- 语法:substr(start,howmany)
- 从开始位置截取到指定长度的字符串
- start 参数区分正负。正值表示下标位置,负值表示从后往前数第几个位置
- howmany 参数必须为正数,也可以不写,不写表示从 start 截取到最后
substring() 方法用于提取字符串中介于两个指定下标之间的字符
- 语法:substring(start,end)
- 参数只能为正数
- 两个参数都是指代下标,两个数字大小不限制,执行方法之前会比较一下两个参数的大小,会用小的数当做开始位置,大的当作结束位置,从开始位置截取到结束位置但是不包含结束位置
- 如果不写第二个参数,从开始截取到字符串结尾
// 截取字符串:三种
// slice(start,end) 从开始位置截取到结束位置,但是不包含结束位置
var str1 = str.slice(3,7);
console.log(str1);//个字符串
var str1 = str.slice(-7);
console.log(str1);//, $%#
// substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符
var str2 = str.substr(6);
console.log(str2);//串,abc, $%#
var str2 = str.substr(6,3);
console.log(str2);//串,a
// substring() 参数必须为整数 小的数当做开始位置,大的当作结束位置
var str3 = str.substring(3,7);
console.log(str3);//个字符串
注意:如果参数取小数会自动省略小数部分
原文链接:https://zhuanlan.zhihu.com/p/366886609
收起阅读 »JavaScript 函数
为什么要有函数?
- 如果要在多个地方求某个数的约数个数,应该怎么做
函数的概念
- 函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行。
- 函数的作用就是封装一段代码,将来可以重复使用
函数声明
- 函数声明又叫函数定义,函数必须先定义然后才能使用
- 如果没有定义函数直接使用,会出现一个引用错误
- 函数声明语法:
function 函数名 (参数) {
封装的结构体;
}
特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行
函数调用
- 调用方法:函数名();
- 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行
- 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关
- 函数可以一次调用,多次执行
函数的参数1
- 我们希望函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化
- 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化
- 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数执行结果根据参数不同,结果也不同
- 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔
案例:累加求和函数
// 函数:封装了一段可以重复调用执行的代码块,通过代码块可以实现大量代码的重复使用
// 1、声明一个累加求和函数
// num1~num2之间所有数之和
function getSum(num1,num2) {
var sum = 0;
for (var i = num1; i <= num2; i++) {
sum += i;
}
console.log(sum);
}
// 2、调用函数
getSum(1,100);
getSum(11,1100);
getSum(321,1212);
函数的参数2
- 函数的参数根据书写位置不同,名称也不相同
- 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传递过来的数据。简称形参
- 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参
- 函数执行过程,伴随传参的过程
函数的参数优点
- 不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么功能,没必要知道内部的结构是什么
- 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要传递什么类型的数据,实现什么功能
函数的返回值
- 函数能够通过参数接收数据,也能够将函数执行结果返回一个值
- 利用函数内部的一个 return 的关键字设置函数的返回值
- 作用 1 :函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行
- 作用 2 : 可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式,函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续可以参加程序,表达式的值就是 return 后面的数据
案例:求和函数
var num1 = Number(prompt("请输入第一个数:"));
var num2 = Number(prompt("请输入第二个数:"));
function sum(a,b) {
return a + b;
}
console.log(sum(num1,num2));
函数的返回值应用
- 函数如果有返回值,执行结果可以当成普通函数参与程序
- 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际参数
- 注意:如果函数没有设置 return 语句,那么函数有默认的返回值 undefined ; 如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined
// 1、return 终止函数
function getSum(num1, num2) {
return num1 + num2;
console.log('return除了返回值还起到终止函数的作用,所以在return后面的代码均不执行!');
}
console.log(getSum(10, 20));
// 2、return 只能返回一个值
function fn(num1,num2) {
return num1, num2; //返回的结果是最后一个值
}
console.log(fn(10, 20));
// 3、 我们求任意两个数 加减乘除 的结果
function getResult(num1, num2) {
return ['求和:' + (num1 + num2), '求差:' + (num1 - num2), '求积:' + (num1 * num2), '求商:' + (num1 / num2)];
}
re = getResult(10, 20);
console.log(re);
// 想要输出多个值可以利用数组
// 4、我们的函数如果有return 则返回的是 return后面的值 如果函数没有 return 则返回undefined
函数表达式
- 函数表达式是函数定义的另外一种方式
- 定义方法:就是将函数的定义、匿名函数赋值给一个变量
- 函数定义赋值给一个变量,相当于将函数整体矮化成了表达式
- 匿名函数:函数没有函数名
- 调用函数表达式,方法是给变量名加 () 执行,不能使用函数名加 () 执行
// 函数的两种声明方式
// 1、利用函数关键字自定义函数(命名函数)
function fn() {
}
fn();
// 2、函数表达式(匿名函数)
// var 变量名 = function() {};
var fun = function(aru) {
console.log('我是函数表达式');
console.log(aru);
}
fun('我是默默!');
// (1)fun是变量名 不是函数名
// (2)函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数
函数数据类型
- 函数是一种独特的数据类型 function -- 是 object 数据类型的一种,函数数据类型
- 由于函数是一种数据类型,可以参与其他程序
- 例如,可以把函数作为另外一个函数的参数,在另一个函数中调用
- 或者,可以把函数作为返回值从函数内部返回
// 函数是一种数据类型,可以当成其他函数的参数
setInterval(function() {
console.log(1);
},1000)
//每隔 1s 输出一个 1
arguments 对象
- JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments 是一个伪数组,因此及可以进行遍历
- 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中
/*
当我们不确定有多少个参数传递的时候 可以用arguments来获取 在JS中 arguments其实是当前函数的
一个内置对象 所有函数都内置了一个arguments对象 arguments对象中存储了传递的所有实参
*/
// arguments的使用
function fn() {
console.log(arguments); //里面存储了所有的实参
}
fn(1, 2, 3);
/*
arguments展示形式是一个伪数组,因此可以进行遍历,伪数组有如下特点:
具有length属性
按照索引方式存储数据
不具有数组的 push pop 等方法
*/
案例:利用 arguments 求一组数最大值
function getMax() {
var max = arguments[0];
var arry = arguments;
for (var i = 0; i < arry.length; i++) {
if (arry[i] > max) {
max = arry[i];
}
}
return max;
}
console.log(getMax(1, 2, 5, 11, 3));
console.log(getMax(1, 2, 5, 11, 3, 100, 111));
console.log(getMax(1, 2, 5, 11, 3, 1212, 22, 222, 2333));
函数递归
- 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象
- 递归的次数太多容易出现错误:超出计算机的计算最大能力
- 更多时候,使用递归去解决一些数学的现象
- 例如可以输出斐波那契数列的某一项的值
// 函数,如果 传入的参数1,返回1,如果传入的是 1 以上的数字,让他返回参数 + 函数调用上一项
function fun (a) {
if (a === 1) {
return 1;
} else {
return a + fun(a - 1);
}
}
// 调用函数
console.log(fun(1));
console.log(fun(2));
console.log(fun(3));
console.log(fun(100));
// 这样我们就用递归做出了 n 以内数累加求和的函数
案例:输出斐波那契数列任意项
// 斐波那契数列(每一项等于前两项之和 1,1,2,3,5,8,13,21,34,55 ···)
// 参数:正整数
// 返回值:对应的整数位置的斐波那契数列的值
function fibo(a) {
if (a === 1 || a === 2) {
return 1;
} else {
return fibo(a - 1) + fibo(a - 2);
}
}
console.log(fibo(1));
console.log(fibo(2));
console.log(fibo(3));
console.log(fibo(4));
作用域
- 作用域:变量可以起作用的范围
- 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量,函数就是变量定义的作用域
- 任何一对花括号 {} 中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域
- 在 es6 之前没有块级作用域的概念,只有函数作用域,现阶段可以认为 JavaScript 没有块级作用域
// js现阶段没有块级作用域 js作用域:局部作用域 全局作用域 现阶段我们js没有块级作用域
// js在ES6的时候新增块级作用域的概念
// 块级作用域就是{}中的区域
if (3 > 2) {
var num1 = 10;
}
console.log(num1);//10
/*
说明js没有块级作用域,外部可以调用{}内声明的变量
*/
全局变量和局部变量
- 局部:变量:定义在函数内部的变量,只能在函数作用域被访问到,在外面没有定义的
- 全局变量:从广义上来说,也是一种局部变量,定义在全局的变量,作用域范围是全局,
- 在整个 js 程序任意位置都能被访问到
- 局部变量退出作用域之后会被销毁,全局变量关闭页面或浏览器才会销毁
函数参数也是局部变量
- 函数的参数本质是一个变量,也有自己的作用域,函数的参数也是属于函数自己内部的局部变量,只能在函数内部被使用,在函数外面没有定义
函数的作用域
- 函数也有自己的作用域,定义在哪个作用域内部,只能在这个作用域范围内被访问,出了作用域不能被访问
- 函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写
作用域链
- 只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域,即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用领域
- 将这样的所有的作用域列出来,可以有一个结构:函数内指向函数外的链式结构。就称作作用域链
遮蔽小于效应
- 程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到第一个变量定义。整个过程中会发生内层变量的效果,叫做“遮蔽效应”
/*
1、只要是代码就至少有一个作用域
2、写在函数内部的局部作用域
3、如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
4、根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,
就被称作作用域链
*/
// 作用域链 : 内部函数访问外部函数的变量 采取的是链式查找的方式来决定取哪个值 这种结构我们称作用域链
// 就近原则
var num = 10;
function fn() {//外部函数
var num = 20;
function fun() {//内部函数
console.log(num);//20
}
fun();
}
fn();
不写 var 关键字的影响
- 在函数内部想要定义新的变量,如果不使用关键字 var ,相当于定义的全局变量。如果全局变量也有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量
- 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局
function fn() {
a = 2;
}
console.log(a);//2
预解析
- JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器执行 JavaScript 代码的时候,分为两个过程:预解析过程和代码执行过程
- 预解析过程:
- 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
- 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
- 先提升 var ,再提升 function
- Javascript 的执行过程:在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码
变量声明提升
- 在与解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程
- 提升过程中,只提升声明过程,不提升变量赋值,相当于变量定义未赋值,变量内存储 undefined 值
- 因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined值
函数声明提升
- 在与解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程
- 在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义成功,后续就可以直接调用函数
- 因此,在 js 中会出现一种特殊现象,在前面调用后定义的函数,不会报错,而且能正常执行函数内部的代码(如果使用 var 声明的函数,在定义函数之前调用函数,会直接报错)
/*
1、
console.log(num);报错
*/
// 2、
console.log(num);//undefined
var num = 10;
// 3、
fn();//11
function fn() {
console.log(11);
}
// 4、
/*
fun();//报错
var fun = function() {
console.log(22);
}
*/
/*
1、我们js引擎运行js 分为两步: 预解析 代码执行
(1) 预解析 js引擎会把js 里面所有的 var 和 function 提升到当前作用域的最前面
(2) 代码执行 按照代码书写的顺序从上往下执行
2、预解析分为 变量预解析(变量提升) 函数与解析(函数提升)
(1) 变量提升 就是把所有的变量声明提升到当前的作用域最前面 不提升赋值操作
(2) 函数提升 把所有的函数声明提升到当前作用域的最前边 不调用函数
*/
提升顺序
- 预解析过程中,先提升 var 变量声明,在提升 function 函数声明
- 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那么在后续代码种出现调用标识符时,内部是函数的定义过程,而不是 undefined
- 如果调用标识符的过程在源代码函数和变量定义的后面,相当于函数名覆盖了一次变量名,结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用的就是变量存的新值
- 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖
函数声明提升的应用
- 函数声明提升可以用于调整代码的顺序,将大段的定义过程放到代码最后,但是不影响代码执行效果
IIFE 自调用函数
- IIFE:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数表达式,表示函数在自定义时就立即调用
- 函数调用方式:函数名或函数表达式的变量名后面加 () 运算符
- 函数名定义的形式不能实现立即执行自调用,函数使用函数表达式形式可以实现立即执行,原因是因为函数表达式定义过程中,将函数矮化成表达式,后面加 () 运算符就可以立即执行
- 启发:如果想实现 IIFE ,可以想办法将函数矮化成表达式
// 关键字定义的方式,不能立即执行
// function fun() {
// console.log(1);
// }();
// 函数表达式,可以立即调用
var foo = function () {
console.log(2);
}();
- 函数矮化成表达式,就可以实现自调用
- 函数矮化成表达式的方法,可以让函数参与一些运算,也就是说给函数前面加一些运算符。
数学运算符:+ - ()
逻辑运算符:!非运算
- IIFE 结构可以封住函数的作用域,在结构外面是不能调用函数的
- IIFE 最常用的时 () 运算符,而且函数可以不写函数名,使用匿名函数
// 通过前面添加操作符可以将我们的函数矮化成表达式
+ function fun() {
console.log(1);
}();
- function fun() {
console.log(1);
}();
(function fun() {
console.log(1);
})();
!function fun() {
console.log(1);
}();
JavaScript 数组
为什么学习数组?
- 之前学习的数据类型,只能存储一个值(比如:Number/String)。如果我们想存储班级中所有学生的成绩,此时该如何存储?
数组的概念
- 所谓数组(Array),就是将多个元素(通常是同一类型)按一定顺序排列放到一个集合中,那么这个集合我们就称之为数组
数组的定义
- 数组是一组有序的数组集合。数组内部可以存放多个数据,不限制数据类型,并且数组的长度可以动态的调整。
- 创建数组最简单的方式就是数组字面量方式
- 数组的字面量:[]
- 一般将数组字面量赋值给一个变量,方便后期对数组进行操作
- 如果存放多个数据,每个数据之间用逗号分隔,最后一个后面不需要加逗号
var arr = [];//创建一个空的数组
var arr1 = [1, 2, '数组', true, undefined, true];
获取数组元素
- 数组可以通过一个 index (索引值、下标)去获取对应的某一项数据,进行下一步操作
- index:从 0 开始,按照整数排序往后顺序排序,例如 0,1,2,3······
- 可以通过 index 获取某项值之后,使用或者更改数组项的值
- 调用数据:利用数组变量名后面直接加 [index] 方式
var arr = ['red', 'orange', 'blue',];//索引号按顺序0 1 2...
console.log(arr[0]); //red
console.log(arr[1]); //orange
console.log(arr[2]); //blue
console.log(arr[3]); //undefined
// 从代码中我们可以发现,从数组中取出每一个元素时,代码是重复的,不一样的是代码的索引值在增加
// 因此我们有更简便的方法一次调用数组中的多个元素
- 注意:如果索引值超过了数组最大项,相当于这一项没有赋值,内部存储的就是 undefined
- 更改数据:arr[index] 调用这一项数据,后面等号赋值更改数据
var arr = [1, 2, '数组', true];
console.log(arr[5]);//undefined
arr[2] = 'haha';
console.log(arr[2]);//'haha'
数组的长度
- 数组有一个 length 的属性,记录的是数组的数据的总长度
- 使用方法:变量.length
console.log(arr.length);
- 数组的长度与数组最后一项的下标存在关系,最后一项的下标等于数组的 length-1
- 获取最后一项数据时,可以这样书写:
console.log(arr[arr.length-1]);
- 数组的长度不是固定不变的,可以发生更改
更改数组长度:
- 增加数组长度:直接给数组 length 属性赋一个大于原来长度的值。赋值方式使用等号赋值
- 或者,可以给一个大于最大下标的项直接赋值,可以强制拉长数组
- 缩短数组长度:强制给 length 属性赋值,后面数组被会直接删除,删除时不可逆的
更改数组长度:
var arr = [1, 3, 5, 7];
arr.length = 10;
console.log(arr);
拉长数组长度:
var arr = [1, 3, 5, 7];
arr[14] = 6;
console.log(arr);
console.log(arr.length);//15
缩短数组长度:
var arr = [1, 3, 5, 7];
arr.length = 3
console.log(arr);//[1,3,4]
console.log(arr.length);//3
数组的遍历
- 遍历:遍及所有,对数组的每一个元素都访问一次就叫遍历。利用 for 循环,将数组中的每一项单独拿出来,进行一些操作
- 根据下标在 0 到 arr.length-1 之间,进行 for 循环遍历
//遍历数组就是把数组的元素从头到尾访问一遍
var arry = ['red','blue','green']
for(var i = 0; i < 3; i++){
console.log(arry[i]);
}
//1.因为索引号从0开始,所以计数器i必须从0开始
//2.输出时计数器i当索引号使用
// 通用写法
var arry = ['red','blue','green']
for(var i = 0; i < arry.length; i++){ //也可以写成: i <= arry.length - 1
console.log(arry[i]);
}
数组应用案例
- 求一组数中的所有数的和以及平均值
var arry = [2, 6, 7, 9, 11];
var sum = 0;
var average = 0;
for (var i = 0; i < arry.length; i++) {
sum += arry[i];
}
average = sum / arry.length;
console.log(sum,average);//同时输出多个变量用逗号隔开
原文:https://zhuanlan.zhihu.com/p/365784347
收起阅读 »JavaScript 常见的三种数组排序方式
一、冒泡排序
冒泡排序 的英文名是 Bubble Sort ,它是一种比较简单直观的排序算法
简单来说它会重复走访过要排序的数列,一次比较两个数,如果他们的顺序错误就会将他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成
这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端

算法思路(以按递增顺序排列为例):
1、我们需要做一个内层循环来比较每对相邻元素的大小,如果前面大于后面,就让他们交换位置,我们要让小的数在前面,大的数在后面
2、当内层循环结束时,在数组最后一位的元素,就一定是这个数组中最大的元素了,这时候除了最后一个元素不用再动以外(所以内层循环每循环一次就可以少循环一次)我们还要再来确定这个数组中第二大的元素,第三大的元素,以此类推,因此我们还需要一层外层循环。如果这个数组有 n 个元素我们就要确定 n - 1 个元素的位置,所以外层循环需要循环的次数就是 n - 1 次
3、只需要内外两层循环嵌套,就可以把数组排序好啦,虽然实现方式可能有很多种,这只是我个人的想法,代码如下,排序功能已封装成函数,请放心食用:
var myArr = [89,34,76,15,98,25,67];
function bubbleSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
for (var j = 0; j < arr.length - i; j++) {
if(arr[j] > arr[j + 1]) {
// 交换两个数的位置
var temp = 0;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(bubbleSort(myArr));
二、选择排序
选择排序 英文叫法是 Selection sort,这也是一种简单直观的排序方法
这种排序首先会在未排序的数组中找到最小或者最大的元素,存放在排序数组的起始位置
然后再从未排序的数列中去找到这个数组中第二大或这第二小的数放在已排序的数之后,以此类推,不断重复直到所有元素排列完毕
算法思路(以按递增顺序排列为例):
1、我们需要内层循环找出未排序数列中的最小值(找最小值可以用之前谁比最小值小谁就替换最小值的思路),循环后找到未排序数列中的最小元素时记录最小的那个元素在数组中的索引值,用索引获得最小值的位置后把它放在数组的第一位,此处注意,如果直接放在第一位会替换第一位数组中原来的元素,我们需要交换最小值的位置,和第一个元素的位置(利用两个变量交换数值的方法)
2、每经过一次内层循环,我们就能确定一个未排序数组中最小值的位置,在确定倒数第二个数的位置时,最后一个数的位置也自然而然地被确定了,因此数组中有 n 个元素我们就需要进行 n - 1 次内层循环,我们就用用外层循环来保持内层循环的重复进行
var myArr = [89,34,76,15,98,25,67];
function selectSort(arr) {
for(var i = 0; i < arr.length - 1; i++) {
//i < arr.length - 1 因为排完倒数第二个,倒数第一个数自然在它正确的位置了
var index = i;
for(var j = i + 1; j < arr.length; j++) {
// 寻找最小值
if(arr[index] > arr[j]){
// 保存最小值索引
index = j;
}
}
// 将未排序中的最小数字,放到未排序中的最左侧
if(index != i) {
var temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
return arr;
}
console.log(selectSort(myArr));
三、插入排序
插入排序 英文称为 Insertion sort ,插入排序也被叫做直接插入排序
它的基本思想是将一个未排序的元素插入到已经排序好的数组中,从而使得已排序的数组增加一个元素,通过插入不断完善已排序数组的过程,就是排序整个数组的过程。
算法思路(以按递增顺序排列为例):
1、因为数组中第一个元素前面没有元素可以进行比较,所以我们从第二个元素开始比较,用 current 变量来进行存储当前要和别人比较的元素,用 preIndex变量 来方便我们去找当前准备插入元素之前的元素
2、内层循环就是按顺序比较插入元素和之前元素的大小,来确定插入元素的位置, preIndex 每比较一次就自减1 ,让准备插入元素和它之前的所有已排序元素都比较一遍,每当待插入元素比前一个数小了,前面的元素就往右挪一个位置,直到前一个数小于待插入数,跳出判断,待插入元素放在前一次判断挪动元素留出的空位上,由于我们提前用 current 保存了要插入的元素,所以要插入的元素不会因为前面的元素覆盖而丢失。
3、每循环一次内层循环,我们就可以确定一个元素的插入位置,但由于我们内层循环是从第二个元素开始的(也就是索引为 1 的元素),因此如果有 n 个元素,我们就需要 n - 1 次内层循环,内存循环我们用外层循环来实现,外层循环就这么被定义完成了
var myArr = [89,34,76,15,98,25,67];
function insertionSort(arr) {
for (var i = 1; i <= arr.length - 1; i++) {
var preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
console.log(insertionSort(myArr));
总结:
选择排序(一种不稳定的排序方法)
优点:移动数据的次数已知(n-1次);
缺点:比较次数多。
冒泡排序
优点:稳定;
缺点:慢,每次只能移动相邻两个数据。
插入排序
优点:稳定,快;
缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量庞大的时候,但用链表可以解决这个问题。
原文:https://zhuanlan.zhihu.com/p/368208410
js 取小数点后几位方法
一 取后两位 为例:
四舍五入
1.toFixed()
Number的toFixed()方法可把 Number 四舍五入为指定小数位数的数字。
const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(2)) // 0.37
复制代码
注意:
.兼容问题
/**
* firefox/chrome ie某些版本中,对于小数最后一位为 5 时进位不正确(不进位)。
* 修复方式即判断最后一位为 5 的,改成 6, 再调用 toFixed
*/
function(number, precision) {
const str = number + '';
const len = str.length;
let last = str[len - 1] // 或者 str.substr(len - 1, len);
if(last == '5') {
let = '6';
str = str.substr(0, len - 1) + last;
return (str - 0).toFixed(precision)
} else {
return number.toFixed(precision)
}
}
或者为:
function toFixed(number, precision) {
const tempCount = Math.pow(10, precision);
let target = number * tempCount + 0.5;
target = parseInt(des, 10) / tempCount;
return target + '';
}
复制代码
.精确问题
/**
* toFixed 有时候会碰到如下精度缺失问题
* 可以使用下面例子的方法解决
* 或者 (test * 100).toFixed(2) + '%';
*/
const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(4)) // 0.3733
console.log((test).toFixed(4) * 100 + '%') // 37.330000000000005%
复制代码
- Math.round()
/**
* 利用Math.round
* 保留两位小数
*/
function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) { return };
tar = Math.round(num * 100) / 100;
}
/**
* 利用Math.round 强制保留两位小数 10 则为 10.00
* 保留两位小数
*/
function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) {return};
tar = Math.round(num * 100) / 100;
let tarStr = tar.toString();
let decIndexOf = tarStr.indexOf('.');
if(decIndexOf < 0) {
tarStr += '.';
decInexOf = tarStr.length;
}
while (tarStr.length <= decIndexOf + 2) {
tarStr += '0';
}
return tarStr;
}
复制代码
不四舍五入
1.先把小数取整 在计算
const test = 1.12 / 3 // 0.37333333333333335
Math.floor(test * 100) / 100 // floor 是向下取整 0.37
复制代码
2.使用正则表达式
const test = 1.12 / 3 // 0.37333333333333335
let target = test + '' // test.toString()
target = target.match(/^\d+(?:\.\d{0, 2}?/)
//输出结果为 0.37。但整数如 10 必须写为10.0000
// 如果是负数,先转换为正数再计算,最后转回负数
作者:maomaoweiw
链接:https://juejin.cn/post/6844903638020816903
收起阅读 »
JavaScript的小技巧
类型转换
数组转字符串
var arr = [1,2,3,4,5];
var str = arr+''; //1,2,3,4,5
字符串转数字
var str = '777';
var num = str * 1; //777
var str = '777';
var num = str - 0; //777
字符串转数字
var str = '666';
var num = str * 1; // 666
向下取整
var num = ~~4.2144235; // 4
var num = 293.9457352 >> 0; // 293
boolean 转换
var bool = !!null; // false
var bool = !!'null'; // true
var bool = !!undefined; // false
var bool = !!'undefined'; // true
var bool = !!0; // false
var bool = !!'0'; // true
var bool=!!''; // true
var bool=!![]; // true
var bool=!!{}; // true
var bool=!!new Boolean('false'); // true
var bool=!!new Boolean('true'); // true
判断对象下面是否有此属性
直接判断
var obj = {a:789};
if(obj.a){ //obj.b ==>789
console.log('运行了') //可以运行
}
if(obj.b){ //obj.b ==>undefined
console.log('运行了') //没有运行
}
var obj2 = {a:false};
if(obj2.a){ //obj.b ==>false
console.log('运行了') //没有运行
}
// 不严谨,如果值为0,undefined,false,null... 也会判断为false
in 操作符
var obj = {a:789};
if('a' in obj){ // 'a' in obj ==>true
console.log('运行了') //可以运行
}
if('b' in obj){// 'a' in obj ==>false
console.log('运行了') //没有运行
}
利用hasOwnProperty
var obj = {a:789};
if(obj.hasOwnProperty('a')){ //==>true
console.log('运行了') //可以运行
}
if(obj.hasOwnProperty('b')){ //==>false
console.log('运行了') //没有运行
}
还有好多好多,得慢慢写
原文:https://zhuanlan.zhihu.com/p/368353172
收起阅读 »uniapp实现$router
作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 _request 等。
众所周知,用 Vue 开发项目,其实就是用的 Vue 全家桶。即 Vue + Vuex + VueRouter 。在代码里的体现就是:
this + this.$store + this.$router/$route
然而由于 uni-app 为了保证跨端同时简化语法,用的是微信小程序那套 API。其中就包括路由系统。因为在 uni-app 中,没有 $router/$route。只有 uni[‘路由方法’]。讲真的,这样做确实很容易上手,但同时也是有许多问题:
- 路由传参数只支持字符串,对象参数需要手动JSON序列化
- 传参有长度限制
- 传参不支持特殊符号如 url
- 不支持路由拦截和监听
因此,需要一个工具来将现有的路由使用方式变为 vue-router 的语法,并且完美解决以上几个问题。
vue-router 的语法这里不再赘述。简单的来说就是将路由的用法由:
uni.navigateTo({
url: `../login/login?data=${JSON.stringify({ from: 'index', time:Date.now() })}`
})
变成:
this.$router.push('/login', {
data: {
from: 'index',
time: Date.now()
}
})
同时传参通过一个 $route 对象。因此我们的需求就是事现一个 $router 和 $route 对象。并给定相应方法。比如调用:
push('/login')
其实就是执行了:
uni.navigateTo({ url:`../login/login ` })
实现起来非常简单:
push 方法接收到 '/login' 将其拼接为 `../login/login` 后调用 uni.navigateTo 就可以。
然而这样并不严谨。此时的 push 方法只能在页面内使用。而不能在 pages 文件夹以外的地方使用,因为这里用的是相对路径。只要改成 `pages/login/login` 就好。
$route 的实现就是在路由发生变化时,动态改变一个公共对象 route 的内部值。
而通过全局 mixin onShow 方法,可以实现对路由变化动态监听。
通过 require.context 预引入路由列表实现更好的错误提示。
最后通过一个页面堆栈数据列表实现 route 实时更新。
最后的代码:
import Vue from 'vue'
export const route = { // 当前路由对象所在的 path 等信息。默认为首页
fullPath: '/pages/index/index',
path: '/index',
type: 'push',
query: {}
}
let onchange = () => {} // 路由变化监听函数
const _$UNI_ACTIVED_PAGE_ROUTES = [] // 页面数据缓存
let _$UNI_ROUTER_PUSH_POP_FUN = () => {} // pushPop resolve 函数
const _c = obj => JSON.parse(JSON.stringify(obj)) // 简易克隆方法
const modulesFiles = require.context('@/pages', true, /\.vue$/) // pages 文件夹下所有的 .vue 文件
Vue.mixin({
onShow() {
const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈
if (pages[0]) { // 当页面栈不为空时执行
let old = _c(route)
const back = pages[0] != route.fullPath
const now = _$UNI_ACTIVED_PAGE_ROUTES.find(e => e.fullPath == pages[0]) // 如果路由没有被缓存就缓存
now ? Object.assign(route, now) : _$UNI_ACTIVED_PAGE_ROUTES.push(_c(route)) // 已缓存就用已缓存的更新 route 对象
_$UNI_ACTIVED_PAGE_ROUTES.splice(pages.length, _$UNI_ACTIVED_PAGE_ROUTES.length) // 最后清除无效缓存
if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回
onchange(route, old)
}
}
}
})
const router = new Proxy({
route: route, // 当前路由对象所在的 path 等信息,
afterEach: to => {}, // 全局后置守卫
beforeEach: (to, next) => next(), // 全局前置守卫
routes: modulesFiles.keys().map(e => e = e.replace(/^\./, '/pages')), // 路由表
_getFullPath(route) { // 根据传进来的路由名称获取完整的路由名称
return new Promise((resolve, reject) => {
const fullPath = this.routes.find(e => RegExp(route + '.vue').test(e))
fullPath ? resolve(fullPath.replace(/\.vue$/, '')) : reject(`路由 ${ route + '.vue' } 不存在于 pages 目录中`)
})
},
_formatData(query) { // 序列化路由传参
let queryString = '?'
Object.keys(query).forEach(e => {
if (typeof query[e] === 'object') {
queryString += `${e}=${JSON.stringify(query[e])}&`
} else {
queryString += `${e}=${query[e]}&`
}
})
return queryString.length === 1 ? '' : queryString.replace(/&$/, '')
},
_beforeEach(path, fullPath, query, type) { // 处理全局前置守卫
return new Promise(resolve => {
this.beforeEach({ path, fullPath, query, type }, resolve)
})
},
_next(next) { // 处理全局前置守卫 next 函数传经来的方法
return new Promise((resolve, reject) => {
if (typeof next === 'function') { // 当 next 为函数时, 表示重定向路由,
reject('在全局前置守卫 next 中重定向路由')
Promise.resolve().then(() => next(this)) // 此处一个微任务的延迟是为了先触发重定向的reject
} else if (next === false) { // 当 next 为 false 时, 表示取消路由
reject('在全局前置守卫 next 中取消路由')
} else {
resolve()
}
})
},
_routeTo(UNIAPI, type, path, query, notBeforeEach, notAfterEach) {
return new Promise((resolve, reject) => {
this._getFullPath(path).then((fullPath) => { // 检查路由是否存在于 pages 中
const routeTo = url => { // 执行路由
const temp = _c(route) // 将 route 缓存起来
Object.assign(route, { path, fullPath, query, type }) // 在路由开始执行前就将 query 放入 route, 防止少数情况出项的 onLoad 执行时,query 还没有合并
UNIAPI({ url }).then(([err]) => {
if (err) { // 路由未在 pages.json 中注册
Object.assign(route, temp) // 如果路由跳转失败,就将 route 恢复
reject(err)
return
} else { // 跳转成功, 将路由信息赋值给 route
resolve(route) // 将更新后的路由对象 resolve 出去
onchange({ path, fullPath, query, type }, temp)
!notAfterEach && this.afterEach(route) // 如果没有禁止全局后置守卫拦截时, 执行全局后置守卫拦截
}
})
}
if (notBeforeEach) { // notBeforeEach 当不需要被全局前置守卫拦截时
routeTo(`${fullPath}${this._formatData(query)}`)
} else {
this._beforeEach(path, fullPath, query, type).then((next) => { // 执行全局前置守卫,并将参数传入
this._next(next).then(() => { // 在全局前置守卫 next 没传参
routeTo(`${fullPath}${this._formatData(query)}`)
}).catch(e => reject(e)) // 在全局前置守卫 next 中取消或重定向路由
})
}
}).catch(e => reject(e)) // 路由不存在于 pages 中, reject
})
},
pop(data) {
if (typeof data === 'object') {
_$UNI_ROUTER_PUSH_POP_FUN(data)
}
uni.navigateBack({ delta: typeof data === 'number' ? data : 1 })
},
// path 路由名 // query 路由传参 // isBeforeEach 是否要被全局前置守卫拦截 // isAfterEach 是否要被全局后置守卫拦截
push(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.navigateTo, 'push', path, query, notBeforeEach, notAfterEach)
},
pushPop(path, query = {}, notBeforeEach, notAfterEach) {
return new Promise(resolve => {
_$UNI_ROUTER_PUSH_POP_FUN(null)
_$UNI_ROUTER_PUSH_POP_FUN = resolve
this._routeTo(uni.navigateTo, 'pushPop', path, query, notBeforeEach, notAfterEach)
})
},
replace(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.redirectTo, 'replace', path, query, notBeforeEach, notAfterEach)
},
switchTab(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.switchTab, 'switchTab', path, query, notBeforeEach, notAfterEach)
},
reLaunch(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.reLaunch, 'reLaunch', path, query, notBeforeEach, notAfterEach)
}
}, {
set(target, key, value) {
if (key == 'onchange') {
onchange = value
}
return Reflect.set(target, key, value)
}
})
Object.setPrototypeOf(route, router) // 让 route 继承 router
export default router
uniapp与flutter,跨平台解决方案你该如何选择
为了做毕设,用了下uniapp与flutter,说真的,这是两款十分优秀的产品,几乎做到了各自领域性能和跨平台的极致。那么这两款产品到底有什么不同,在选型的时候应该如何取舍,这是我写这篇文章的目的。
uniapp与flutter都是为了解决跨平台问题的框架
uniapp是从h5 app到小程序一步步发展过来的,也就是走的html的路线。
html从最早的网页套壳一步步发展至今,为了解决早期套壳的体验问题,我们尝试用js代码调用原生接口,与原生进行交互,出现了一系列如React Native,Cordova,Weex,Framework7,MUI之类的框架,这些框架的出现进一步丰富h5应用的功能。但是这些技术要求很高的优化技巧,要走很多坑,在ios的体验尚可,但是Android上由于更新维护问题,js引擎差别很大,早期Android的js引擎极差,这些框架使用体验都不好,当然也有硬件方面的原因。而且Android上webview存在性能瓶颈,复杂应用不做预加载的情况下使用体验真的不好。后来为了使体验达到h5所能做的极致,小程序出现了,为了性能,屏蔽了dom,规定了独特的规范,按照这些规范去写,编译时框架提前给你优化好,事实证明这样做确实可以提高h5应用的使用体验。
uniapp延续了小程序的思路,和vue结合,屏蔽dom,提前优化,确实很好,也做到了跨平台,这是一款极为优秀的跨各种小程序的解决方案,与它自家的h5+结合也是一个还算不错的h5+ app的前端框架。但是uniapp的定位中有一个极大的问题,就是小程序与h5 app之间的距离太大了,强跨的体验真是极差,得不偿失。举个栗子,3d渲染,多人视频,nfc写卡,这种小程序完全做不到,当然uniapp也可以调h5+ runtime,但是一个复杂的移动端应用可能会加各种各样的东西,你完全预料不到可能出现什么需求,并且这些需求越来越多的情况下,小程序端与移动端分开维护是必然的结果,强行结合只能是结构混乱,难以维护。那么如果分开维护,uniapp与前面提到的那些框架并没有明显优势。
那么接着说flutter,flutter与h5技术栈的思路完全不同,JSCore,V8再怎样优秀,也始终解决不了JavaScript本身语法缺陷和运行在浏览器的事实。
===========================
这里我之前写flutter用dart做了一个渲染引擎,有人言辞激烈的抨击了我的错误,后来我仔细看了一下资料。

官网上是这样说的
Flutter is built with C, C++, Dart, and Skia (a 2D rendering engine).See this architecture diagram for abetter picture of the main components.
确实,dart只是用来组织各种控件的一个工具,这个图形渲染是用了这个叫Skia的图形库
Skia is an open source 2D graphics library which provides common APIs that work across a variety of hardware and software platforms.
这个Skia,Google旗下,开源2D图形库,提供了多种软硬件平台的通用API。
确实是我的错,没调查清楚,但是这个方式还是令我觉得,很难受。
=================================
也就是说flutter和cocos,unity3d一样,完全可以用来写游戏,突破60fps,而且自己渲染,大大减少了与原生的通信次数,并且使用 Platform Channels 来跟系统通信大大丰富了一些偏门功能的应用,去组件库看了下tcp直连mqtt都支持了,刚好毕设会用到,开心。
所以如果你需要跨平台,技术选型时遇到问题
1.看需求
如果你的应用需求足够简单,像小程序之类的完全可以做到,选uniapp。因为说真的,像点单这种功能,谁没事愿意专门装个肯打鸡,coco之类在手机上,反正我去点单的时候,能用小程序我就不会再装app了,如果有人愿意装app,稍微改改顺便出个app版看着比较好看。
如果你的需求复杂,必然要分开维护,还是和之前一样,uniapp是一个极好的跨各种小程序的解决方案,一次编译,微信小程序,支付宝小程序,百度小程序,多端运行。那app端你可以再选择h5或者flutter。
如果你需要适配横屏,建议用flutter,横屏的交互加上material design的加持,这样和桌面端就没有太大区别了,目前flutter已经可以编译运行在Windows和linux上,虽然目前还很不完善,但是Google的野心和背书能力让我觉得flutter的野心不止于此。未来能附赠一套桌面端,意外之喜。
2.学习成本
flutter的学习成本主要在Dart,而uniapp主要在vue。说真的,我之前做Android和JavaWeb的,Java转Dart真的没有压力,有人说flutter嵌套太多,安卓xml布局嵌套不多吗,公司现在维护的ERP系统jQuery写的跟使一样,各种+ " append。
而我一个传统Java使用者刚开始遇到vue真的难受了好一阵子,这个this的真是vue里令我最难受的,使一样。推荐周围同学学uniapp,学过C++,Java的普遍反映也是vue看不懂。你们再也不是像jQuery一样好单纯好不做作的前端了。
总之前端的uniapp学习成本低,学过后端Java,C++的,flutter上手成本低。
3.社区
刚开始Google要出Fuchsia OS的时候我还嗤之以鼻,真当程序狗们都会乖乖听你话吗,那win phone坟头草都老高了。没想到啊,你们早在苹果骂安卓垃圾的时候就想着今天了吧。
Google在安卓界的背书能力感觉跟Spring在JavaWeb界的背书能力不逞多让,只要Android和Fuchsia不死,Flutter应该不会有太大问题,而且Flutter的社区是真的真的真的很活跃啊,github上问题的解决速度和出视频的速度真是令我叹为观止。
相比之下DCloud出MUI到现在不愠不火就让我不禁对uniapp有些担心,虽然微信,支付宝在后面背书,希望一群国内一线大厂们能给力点吧。而且我在uniapp提的问题一个多月了,无人问津
【报Bug】使用小程序组件,当参数为函数时,传不过去 - DCloud问答
希望你们珍惜你们的银牌赞助者。而且出视频的速度一言难尽,看B站居然没有,讲道理一个好的教学视频真的很重要,干啃API在学习时真是费力不讨好的事情,你学习的思路和文档的思路是不一样的。不过uniapp的QQ群倒是很火,不管怎样,一个国产的优秀产品,希望你们能有一个好的未来。
原文:https://zhuanlan.zhihu.com/p/55466963
收起阅读 »uni-app 的使用体验总结
[实践] uni-app 的使用总结
最近使用 uni-app 的感受。
使用体验
没用之前以为真和 Vue 一样,用了之后才知道。有点类似 Vue 和 小程序结合的感觉。写类似小程序的标签,有着小程序和 Vue 的生命周期钩子。对比 uni-app 文档和微信小程序的文档,不差多少,只是将 wx => uni
,熟悉 Vue 和 小程序可以直接上手。
如果看过其他小程序的文档,可以发现,文档主要的三大章节就体现在框架、组件、API 。
uni-app 需要注意看注意事项,文档给出了和 Vue 使用的区别。例如动态的 Class 与 Style 绑定,在 H5 能用,APP 和小程序的体现就不一样。
配置项跟着文档来,开发环境也是现成的,下载 HBuilderX 导入项目就能运行,日常开发习惯了 VSCode,所以 HBuilderX 的主要作用就是用来打包 APK 和起各个端的服务,coding 的话当然还是用 VSCode。
路由
uni-app 的路由全部配置在 pages.json
文件里,就会导致多人开发的时候,路由无法拆分,如果处理的不好,就会发生冲突。
导航
导航栏需要注意的一个问题就是不同端的展示形式会不同,所以要处理兼容问题,导航栏可以自定义,用原生,框架,插件但是兼容性都不同,多端需求一定要在不同设备跑一下看效果。
例如在小程序和 APP 中,原生导航栏取消不了,就不能用自定义的导航栏,要在 pages.json
中配置原生导航栏。
兼容方法就是用 uni-app 提供的条件编译,处理各端不同的差异,我们支付的业务逻辑也是通过条件编译,区分不同端调用不同的支付方式。
生命周期
分为 应用的生命周期
、页面的生命周期
、组件的生命周期
。写过小程序和 Vue 的很好理解,大致上和 Vue 的还是差不多的,页面生命周期针对当前的页面,应用生命周期针对小程序、APP。这些过程可能都要踩一下!
网络请求和环境配置
官方的 uni.request
虽然封装好了基本的请求,但是没有拦截,我们开始也是自己在这基础上加了层壳,简单的封装发送请求。当然也可以选择第三方库的使用,如 flyio、axios。
我们是前端自己封装了 HTTP 请求,并且统一接口的请求方式,所有的接口放到 api.js 文件中进行统一管理。这样大家在页面请求接口的时候风格才统一,包括约定好请求拦截和响应拦截,具体拦截的参数和后台约定好。
资源优化
- 暂时接触不到 Webpack 之类的资源打包优化,但是文档中有提到资源预取、预加载、treeShaking 只需要在配置文件中设置即可,或者在开发工具勾上。小程序也是勾选自动压缩混淆。
- 删除没用到文件和图片资源,因为打包的时候是会算进去的,比如
static
目录下的资源文件都会被打包,而且图片资源太大也不好。 - uni-app 运行时的框架主库
chunk-vendors.js
文件是经过处理的,部署做gzip
。
Web-View 组件
在 uni-app 中使用 Web-View,可以使用本地的资源和网络的资源,不同平台也是有差异的,小程序不支持本地 HTML,且小程序端 Web-View 组件一定有原生导航栏。
需要注意的是网页向应用 postMessage
的时候需要引入 uni.web-view.js
,不然是没办法通信拿不到数据。
TODO: 这个坑后面再详细总结下!
全局状态
最开始是直接使用类似小程序的 globalData 来管理我们的全局状态,但是后面发现需求一多,加了各种东西之后,需要取这个状态的时候就很痛苦,做为程序猿嘛,都想偷懒吖,每次都得引入一下 getApp().globalData.data
这样很繁琐可不行,就替换成了 Vuex,需要取这个变量的时候,直接 this.vuex_xxxx
就能拿到这个值。
有段时间重写了 HTTP 请求部分和全局状态管理部分。
小程序中要在每一个页面中添加使用共有的数据,可以有三种方式解决。
Vue.prototype
它的作用是可以挂载到 Vue 的所有实例上,供所有的页面使用。
// main.js
Vue.prototype.$globalVar = "Hello";
然后在 pages/index/index
中使用:
<template>
<view>{{ useGlobalVar }}</view>
</tempalte>
<script>
export default {
data (){
return {
useGlobalVar: $globalVar
}
}
}
</script>
globalData
<!-- App.vue -->
<script>
export default {
globalData:{
data:1
}
onShow() {
getApp().globalData.data; // 使用
getApp().globalData.data = 1; // 更新
};
</script>
Vuex
Vuex 是 Vue 专用的状态管理模式。能够集中管理其数据,并且可观测其数据变化,以及流动。
之前看到一个通俗化比喻:用交通工具来比喻项目中这几种描述全局变量的方式。
下面列举这些方式通俗的理解状态:
Vue 插件 vue-bus
可以来管理一部分全局变量(叫应用状态吧),学习后发现,bus(中文意思:公交车)这名字取得挺形象的。
先罗列一下这些方式,不过这种分类并不严谨。
1、VueBus:公交车 2、Vuex:飞机 3、全局 import
- a.
new Vue()
:专车; - b.
Vue.use
:快车; - c.
Vue.prototype
:顺风车。
4、globalData:地铁
首先 VueBus
,像公交车一样灵活便捷,随时都可以乘坐;表现在代码里,很轻便,召之即来,缺点就是不好维护,没有一个专门的文件去管理这些变量。想象平时等公交车的心情,知道它回来,但不知道它什么时候来,给人一种很不安的感觉。
而 Vuex
,它像飞机,很庄重,塔台要协调飞机运作畅顺,飞机随时向地面报告自己的位置,适合用在大型项目。表现代码中,就是集中式管理所有状态,并且以可预测的方式发生变化。也对应着飞机绝对不能失联的特点。
第三种方式是全局 import
,分三种类型,分别是:new Vue()
、Vue.use()
、Vue.prototype
。可以用网约车来比喻,三种类型分别对应:专车、快车、顺风车。都足够灵活,表现在代码里:一处导入,处处可用。
再分别说明:
new Vue()
就像滴滴的礼橙专车,官方运营,安全可靠。表现在代码里,就是只有 Vue 官方维护的库才能使用这种方式。
Vue.use()
就像快车,必须符合滴滴的规范,才能成为专职司机。表现在代码中,就是导入的插件(或者库)必须符合 Vue 的写法(即封装了 Vue 插件写法)。
Vue.prototype
像顺风车,要求没上面两个那么严,符合一般 js 写法就行,就像顺风车的准入门槛稍稍低一点。
当然,uni-app 的项目里还有可以用 globalData
定义全局变量,非要比喻,可以用地铁,首先比 vue-bus
更好管理维护,想象地铁是不是比公交更可靠;其次比 Vuex 更简单,因为 globalData 真的就是简单的定义一些变量。
globalData 是微信小程序发明的,Vue 项目好像没有对应的概念,但是在 uni-app 中一样可用。
上面说到,这种分类方式不严谨,主要体现在原理上,并不是简单的并列关系或包含关系。
插件市场
uni-app 的主要特色也源自于它的插件市场十分丰富。
用得比较好的组件:
uView:我们用了这个库的骨架屏。这个库还是有很多技巧可以学到的。
https://www.uviewui.com/js/intro.html
ColorUI-UniApp:是个样式库,不是组件库。
https://ext.dcloud.net.cn/plugin?id=239
答题模版:左右滑答题模版,单选题、多选项,判断题,填空题,问答题。基于 ColorUI 做的。
https://ext.dcloud.net.cn/plugin?id=451
uCharts 高性能跨全端图表:
https://ext.dcloud.net.cn/plugin?id=271
最后:各端的差异性,很多东西,H5 挺好的,上真机就挂了,真机好着的,换小程序就飘了,不同小程序之间也有差异,重点是仔细阅读文档。
云打包限制,云打包(打 APK) 的每天做了限制,超出次数需要购买。
虽然可能一些原生可以实现的功能 uni-app 实现不了,不过整体开发下来还行,很多的坑还是因为多端不兼容,除了写起来麻烦一点,基本上都还是有可以解决的策略。比之前用 Weex 写 APP 开发体验好一点,比 React Native 的编译鸡肋一点(这点体验不是很好),至于 Flutter 还没有试过,有机会的话会试一下。
原文:https://zhuanlan.zhihu.com/p/153500294
收起阅读 »使用uniapp开发项目来的几点心得体会
先说一下提前须要会的技术
要想快速入手uniapp的话,你最好提前学会vue、微信小程序开发,因为它几乎就是这两个东西的结合体,不然,你就只有慢慢研究吧。
为什么要选择uniapp???
开发多个平台的时候,对,就是开发多端,其中包括安卓、IOS、H5/公众号、微信小程序、百度小程序...等其它小程序时,如果每个平台开发,人力开发成本高,后期维护也难,原生开发周期也长,那Unipp就是你的优先选择,官方是这样介绍的~哈~ 先来说一下uniapp的优点
uniapp优点
优点一,多端支持
当然是多端开发啦,uni-app是一套可以适用多端的开源框架,一套代码可以同时生成ios,Android,H5,微信小程序,支付宝小程序,百度小程序等。
优点二,更新迭代快
用了它的Hbx你就知道,经常会右下角会弹出让你更新,没错,看到它经常更新,这么努力的在先进与优化,还是选良心的了。
优点三,扩张强
你可以把轻松的把uniapp编译到你想要的端,也可以把其它端的转换成uniapp,例如微信小程序,h5等;如果开发app的时候,前端表现不够,你还可以原生嵌套开发。
优点四,开发成本、门槛低
不管你是公司也好,个人也好,如果你想开发多终端兼容的移动端,那uniapp就很适合你,不然以个人的能力要开发多端,哈哈... 洗洗睡觉吧。
优点五,组件丰富
社区还是比较成熟,生态好,组件丰富,支持npm方式安装第三方包,兼容mpvue,DCloud有大量的组件供你使用,当然付费的也不贵,你还可以发布你开发的,赚两个鸡腿钱还是可以的。
开发上的优点暂且不说,大体上的有这么一些,接下来说一下开发过程中的缺点
uniapp缺点
缺点一:爬坑
每个程序前期肯定都会有很多的坑,这里点明一下:腾讯,敢问谁没在微信开发上坑哭过,现在不也爬起来了,2年前有人提的bug,你现在去看,他依然在那,不离不弃呀。uniapp坑也有,一般的都有人解决了,没解决的,你就要慢慢的去琢磨了,官方bug的话,提交反馈,等官方修复。
缺点二:某些组件不成熟
我说的是某些官方组件,像什么地图组件,直播组件等,你要在上面开发一些特别功能的话,那真的是比较费神的。
缺点二:nvue有点蛋疼
某些组件或某些功能,官方明确说,建议用nvue开发,那么问题来了,nvue有很多的局限,特别是css,很多都不支持,什么文字只能是text,只支持class样式,很多的,要看文档来。
暂时从使用上的总结就这么一些,如果你有不同的见解,留言交流交流~~
原文:https://zhuanlan.zhihu.com/p/336773995
收起阅读 »uni-app 悬浮框动效
<view class="menu" :class="{active:menuFlag}">
<image src="../../static/svg/1.svg" class="menuTrigger" @tap="clickMenu"></image>
<image src="../../static/svg/2.svg" class="menuItem menuItem1"></image>
<image src="../../static/svg/3.svg" class="menuItem menuItem2"></image>
<image src="../../static/svg/4.svg" class="menuItem menuItem3"></image>
</view>
.menu{
position: fixed;
width: 110rpx;
height: 110rpx;
bottom: 120rpx;
right: 44rpx;
border-radius: 50%;
}
.menuTrigger{
position: absolute;
top: 0;
left: 0;
width: 70rpx;
height: 70rpx;
background-color: green;
border-radius: 50%;
padding: 20rpx;
cursor: pointer;
transition: .35s ease;
}
.menuItem{
position: absolute;
width: 50rpx;
height: 50rpx;
top: 10rpx;
left: 10rpx;
padding: 20rpx;
border-radius: 50%;
background-color: white;
border: none;
box-shadow: 0 0 5rpx 1rpx rgba(0,0,0,.05);
z-index: -1000;
opacity: 0;
}
.menuItem1{
transition: .35s ease;
}
.menuItem2{
transition: .35s ease .1s;
}
.menuItem3{
transition: .35s ease .2s;
}
.menu.active .menuTrigger{
transform: rotateZ(225deg);
background-color: pink;
}
.menu.active .menuItem1{
top: -106rpx;
left: -120rpx;
opacity: 1;
}
.menu.active .menuItem2{
top: 10rpx;
left: -164rpx;
opacity: 1;
}
.menu.active .menuItem3{
top: 126rpx;
left: -120rpx;
opacity: 1;
}
data() {
return {
mask: false,
menuFlag: false,
}
},
clickMenu(){
this.menuFlag = !this.menuFlag;
},
原文链接:https://zhuanlan.zhihu.com/p/364244176
收起阅读 »async/await 的错误捕获
一、案发现场
为了更好的说明,举一个很常见的例子:
function getData(data) {
return new Promise((resolve, reject) => {
if (data === 1) {
setTimeout(() => {
resolve('getdata success')
}, 1000)
} else {
setTimeout(() => {
reject('getdata error')
}, 1000)
}
})
}
window.onload = async () => {
let res = await getData(1)
console.log(res) //getdata success
}
这样写可以正常打印getdata success
但是如果我们给getData传入的参数不是1,getData会返回一个reject的Promise,而这个地方我们并没有对这个错误进行捕获,则会在控制台看见这样一个鲜红的报错Uncaught (in promise) getdata error
二、尝试捕获它
1. 踹一脚
捕捉错误,首先想到的就是“踹一脚”:
window.onload = async () => {
try {
let res = await getData(3)
console.log(res)
} catch (error) {
console.log(res) //getdata error
}
}
看似问题已经被解决,但是如果我们有一堆请求,每一个await
都需要对应一个trycatch
,那就多了很多垃圾代码。或许我们可以用一个trycatch
将所有的await
包起来,但是这样就很不方便对每一个错误进行对应的处理,还得想办法区分每一个错误。
2. then()
因为返回的是一个Promise,那我们首先想到的就是.then()
和.catch()
,于是很快就能写出以下代码:
window.onload = async () => {
let res = await getData(3).then(r=>r).catch(err=>err);
console.log(res) //getdata error
}
这样看起来比“踹一脚”高大上一点了……
三、有没有更好的方式
上面那种方法是有一定问题的,如果getData()
返回是resolve
,res
则是我们想要的结果,但是如果getData()
返回是reject
,res
则是err
,这样错误和正确的结果混在一起了,显然是不行的。
window.onload = async () => {
let res = await getData(3)
.then((res) => [null, res])
.catch((err) => [err, null])
console.log(res) // ["getdata error",null]
}
这种方式有的类似error first
的风格。这样可以将错误和正确返回值进行区分了。但是这种方式会让每一次使用await
都需要写很长一段冗余的代码,因此考虑提出来封装成一个工具函数:
function awaitWraper(promise) {
return promise.then((res) => [null, res])
.catch((err) => [err, null])
}
window.onload = async () => {
let res = await awaitWraper(getData(3))
console.log(res) // ["getdata error",null]
}
好多了,就先这样吧。
原文链接:https://zhuanlan.zhihu.com/p/114487312
收起阅读 »