zhl1232 / daily-frontend Goto Github PK
View Code? Open in Web Editor NEW每日一个前端知识点或者面试题,共同努力,希望一年后可以看到一个不一样的自己。
每日一个前端知识点或者面试题,共同努力,希望一年后可以看到一个不一样的自己。
首先,感性的认知一下对象。
Object在英文中表示东西, (可看见或者触摸到的)实物。可以理解为实体的一切事物。
编程语言里的对象是顺着人类思维模式产生的一种抽象(于是面向对象编程也被认为是:更接近人类思维模式的一种编程范式)。
对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。
先来说第一类属性,数据属性也就是数据描述符。
第二类属性,访问器属性也就是存取描述符
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值。
var o = {
a: 7,
get b() {
return this.a + 1;
},
set c(x) {
this.a = x / 2
}
};
console.log(o.a); // 7
console.log(o.b); // 8
o.c = 50;
console.log(o.a); // 25
我们可以使用内置函数 Object.getOwnPropertyDescripter 来查看对象属性,如以下代码所示:
var o = { a: 1 };
o.b = 2;
//a 和 b 皆为数据属性
Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
如果我们要想改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty,示例如下:
var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//a 和 b 都是数据属性,但特征值变化了
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
o.b = 3;
console.log(o.b); // 2
其实Object.freeze(obj)就是改变obj的writable,configurable为false
let res = 100 == '100' ? 'yes' : 'no' // 'yes'
let res = 100 === '100' ? 'yes' : 'no' // 'no'
但有时候三元运算符仍然很复杂,我们可以使用逻辑运算符 && 和 || 来替代,让代码更简洁一些。这种技巧通常被称为“短路求值”。假设我们想要返回两个或多个选项中的一个,使用 && 可以返回第一个 false。如果所有操作数的值都是 true,将返回最后一个表达式的值。
let a = 1, b = 2, c = 3;
console.log( a && b && c ) // 3
console.log( null && 0 ) // null
使用 || 可以返回第一个 true。如果所有操作数的值都是 false,将返回最后一个表达式的值。
let a = 1, b = 2, c = 3;
console.log( a || b || c ) // 1
console.log( null && 0 ) // 0
除了短路求值,在实际开发中, || 还可以用来做数据边界兜底
比如我们想要返回一个变量的 length,但又不知道变量的类型或者变量拿到的是undefined。
return ( Array.isArray(data) || typeof data === 'string' || [].length)
这样,如果data是数字或者不是拥有length的对象,那么返回0,如果有length返回该对象的length。这样不会因为未知属性而造成程序无法运行。
对象转布尔都为真,[]是对象所以也为真,[].legnth 为 0 。
但数组转数字不是这样,[] => 0 , [1] => 1 , [1, 2, 3] => NaN
还有访问嵌套对象属性时遇到的问题,你可能不知道对象或某个子属性是否存在,所以经常会碰到让你头疼的错误。假设我们想要访问 this.state 中的一个叫作 data 的属性,但 data 却是 undefined 的。在某些情况下调用 this.state.data 会导致 App 无法运行。
return (this.state.data || 'default')
这里 default 写成你预期需要的类型。这样不管得到的值是不是有问题,程序都可以正常运行。
console.log( 2 ** 3 ) // 8
但要注意不要把这个运算符于 ^ 混淆在一起了,^ 通常用来表示指数运算,但在 JavaScript 中,^ 表示位异或运算。在 ES7 之前,可以使用位左移运算符 << 来表示以 2 为底的幂运算:
// 下面的表达式是等效的
Math.pow(2, n)
2 << (n - 1)
2 ** n
例如,2 << 3 = 16 等同于 2 ** 4 = 16。
我们可以使用 Math.floor()、Math.ceil() 或 Math.round() 将浮点数转换成整数,但有另一种更快的方式,即使用位或运算符 |。
console.log(-139.1 | 0)
console.log(-139.7 | 0)
console.log(139.6 | 0)
console.log(139.4 | 0)
// 上面的结果都是 139
也可以使用~~ 达到同样的效果。
console.log( ~~-139.1 )
console.log( ~~-139.7 )
console.log( ~~139.6 )
console.log( ~~139.4 )
// 结果一样都是向下取整
按位异或和位移操作符也可以取整,可以自己去尝试下。
let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(array.slice(-1)); // Result: [9]
console.log(array.slice(-2)); // Result: [8, 9]
console.log(array.slice(-3)); // Result: [7, 8, 9]
console.log(JSON.stringify({ alpha: 'A', beta: 'B' }, null, '\t'));
// 或者这样
console.log(JSON.stringify({ alpha: 'A', beta: 'B' }, null, 4));
// Result:
// '{
// "alpha": A,
// "beta": B
// }'
现在的代码都会经过 uglify 压缩混淆,
有时你可能在生产环境中遇到问题,但是你的source maps没有部署在服务器上。Chrome 可以将你的 Javascript 文件美化为更易阅读的格式。虽然代码不会像你的真实代码那样有用 – 但至少你可以看到发生了什么。
用 Sources 打开混淆代码后最下面有个{},点一下就会格式化代码。
一旦执行到这行代码,Chrome 会在执行时自动停止。 你甚至可以使用条件语句加上判断,这样可以只在你需要的时候运行。后续的调试步骤和断点调试没什么区别。
if (thisThing) {
debugger;
}
8 使用 console.time() 和 console.timeEnd() 来标记循环耗时
console.time('Timer1');
var items = [];
for(var i = 0; i < 100000; i++){
items.push({index: i});
}
console.timeEnd('Timer1');
// Timer1: 25.840087890625ms
// 会打印出从开始到结束用了多少时间
<template>
<div>
<button @click="handleNameChange">change this.name</button>
</div>
</template>
<script>
let name = "world";
export default {
data() {
this.name = name;
return {
};
},
methods: {
handleNameChange() {
this.name = "vue" + Date.now();
console.log("this.name 发生了变化,但是并没有触发组件更新", this.name);
},
}
};
</script>
可以自己传到子组件里测试, this.name 改变了,但视图并不会更新.
<button @click="handleInfoChange">change this.info</button>
data() {
return {
info: {},
};
},
handleInfoChange() {
this.info.number = 1;
console.log("this.info 发生了变化,但是并没有触发组件更新", this.info);
},
<button @click="handleListChange">change this.list</button>
data() {
return {
list: [],
};
},
handleListChange() {
this.list[0] = 123
console.log("this.list发生了变化,但是并没有触发组件更新", this.list);
},
先说处理方法,
// 1. 把对象里的属性都写进去,这样都会触发响应式
data() {
return {
info: {
number: undefined
},
};
},
handleInfoChange() {
this.info.number = 1;
},
// 2. 用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性
data() {
return {
info: {
},
};
},
handleInfoChange() {
this.$set(this.info, 'number', 1)
},
3.解决方法两种
// 1. Vue.set(vm.items, indexOfItem, newValue)
this.$set(this.list, 0, 123);
// 2. 变异数组方法
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
// 这些方法是 vue 框架重写过的,使用这些方法会触发响应式
this.list.splice(0 , 1, 123)
function fn(person) {
person.age = 23
person = {
name: 'zzz',
age: 28
}
return person
}
const p1 = {
name: 'xxx',
age: 25
}
const p2 = fn(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
参数传值是指函数调用时,给函数传递配置或运行参数的行为,包括通过call、apply 进行传值。
基本类型和引用类型在变量复制的时候存在区别:
javascript 中所有函数参数都是按值传递,都是把形参复制给实参,只是基本数据类型复制的是原始值,而引用类型复制的是堆内存的地址。
let a = 1
function foo(x) {
x = 2
console.log(x)
}
foo(a) // 2
console.log(a) // 1
它们在全局上下文和foo的上下文中各自保存了值1,且相互之间互不影响,我们对 a、x的读写操作,操作的是他们各自的值。
let a = {
abc: 1
}
function foo(x) {
x.abc = 2
console.log(x.abc)
}
foo(a) // 2
console.log(a.abc) // 2
上面的代码很容易得出一个错误的结论,对象传值是按引用传递的。
对象a的引用被传递到函数foo内部, 函数内部变量x指向全局变量a,从而实现了引用的传递,所以变量x和变量a读写的是同一个对象。
如果是按引用传递那下面这个例子就懵比了:
let a = {
abc: 1
}
function foo(x) {
console.log(x) // {abc: 1}
x = 2
console.log(x) // 2
}
foo(a)
console.log(a.abc) // 1
为什么会出现a、x在指向同一个对象后,对x赋值又没有改变原对象的值呢?
因为这里对象传递给实参是按共享传递(call by sharing)的,根据引用类型变量复制的特点(上面描述过):
foo 函数执行时, 形参 x 的值是传进去的对象 a 的内存地址引用,即在变量对象创建阶段x保存的是一个对象的堆内存地址。此时 a、x 都指向同一对象。 接着在函数的执行阶段,代码的第二行将原始数据类型 2 赋值给 x,导致 x 不再保存原先的堆内存地址转而保存一个原始值,再次访问 x 的时候是访问对 x 最后一次赋值的原始值。
所以对 x 的赋值会改变上下文栈中标识符 x 保存的具体值
let a = {
abc: 1
}
function foo(x) {
x.abc = 99
console.log(x) // {abc: 99}
x = 2
console.log(x) // 2
}
foo(a)
console.log(a) // {abc: 99}
在 foo 函数内部修改对象 x 的属性,会导致 x、a 指向的对象被修改,因为它们指向同一个堆地址。
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种: null,undefined,boolean,number,string,symbol
首先我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。
console.log(3 === new Number(3)); // false
console.log(typeof 3); // number
console.log(typeof new Number( 3 )); // object
Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。
console.log(typeof String('str')); // string
console.log(typeof new String( 'str' )); // object
Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。
原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString() 会抛出错误
但为什么 '1'.toString() 是可以使用的。
其实是 . 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。在该临时对象调用函数返回函数操作结果后,将该对象丢弃。
null 代表赋值了,但内容为空,undefined 表示未定义。
另外对于 null 来说。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
一般建议用 void 0 代替 undefined ,因为在 ES5 之前 undefined 是一个变量,而并非是一个关键字,为了避免无意中被修改,建议用 void 0 代替 undefined
let a
// 我们也可以这样判断 undefined
a = undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a = void 0
// 自执行函数建议像下面这样写
void function() {
console.log('hello')
}()
// 这样 function 返回 undefined, void 语义上也比较容易理解
// 还会避免没加 ; 引起的编译错误
console.log(true = new Boolean(true)); // false
因为 true 是基本类型,new Boolean(true)是一个对象
JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无论你在 string 类型上调用何种方法,都不会对值有改变。
就是所有的字符串方法得到的都是副本。
所有的原始类型都不可以改变,只可以被替换。
JavaScript 中的 Number 类型有 (2^64 - 2^53+3) 个值。JavaScript 采用 IEEE 754 双精度版本(64 位)。
指数为 2^e - 1 且尾数的小数部分全 0,这个数字是 ±∞。(符号位决定正负)
指数为 2^e - 1 且尾数的小数部分非 0,这个数字是 NaN。
其中 NaN,占用了 9007199254740990 位,即(2^53-2);±∞ 占用两位。但是表示了三个直观的量。
JavaScript 中的数字是 64-bits 的双精度,所以加减一下,一共有(2^64 - 2^53 + 3)个值。
64 位双精度在计算机中存储占用 8 字节,64 位,有效位数为 16 位。其中符号位,指数位和尾数部分分别为 1, 11, 52。取值范围取决于指数位,计算精度取决于尾数位(小数)。
小数位是 52 位(二进制),换算为十进制则只能百分百能保证 15 位。超过该精度(二进制 52 位,十进制 15 位)的小数运算将会被截取,造成精度损失和计算结果的不准确。
console.log( 0.000000000000001 <= Number.EPSILON ); // false
console.log( 0.0000000000000001 <= Number.EPSILON ); // true
所以 JavaScript 提供的最小精度值 Number.EPSILON 为 2.220446049250313e-16 也就是
小于 16 位小数(10 进制)。
console.log( 2.220446049250313e-16.toString(2) );
// 0.0000000000000000000000000000000000000000000000000001
// 52位小数(2进制)
计算机计算都是用二进制的。
问:要把小数装入计算机,总共分几步?你猜对了,3 步。
第一步:转换成二进制。
第二步:用二进制科学计算法表示。
第三步:表示成 IEEE 754 形式。
0.1 二进制计算过程
0.1*2=0.2========取出整数部分0
0.2*2=0.4========取出整数部分0
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
……
得到一个无限循环的二进制小数 0.000110011…
用科学计数法表示
0.000110011(0011) == 1.100110011(0011)*2^-4 // (0011) 表示循环
任何一个 r 进制数 N 都可以写成(N)r=(+/-)S*r(+/-e)这种科学计数法
其中 N 表示需要表示的数,r 表示进制,S 表示尾数,N 的有效位数字,e 表示阶码,代表小数点的位置
表示成 IEEE 754 形式
组合在一起就是 0-01111111011-100110011(0011)
因为 IEEE 754 64 位只能存储 52 位尾数,剩下的需要舍入。
因此 0.1 实际存储时的位模式是 0-01111111011-1001100110011001100110011001100110011001100110011010;
0.2 同理得到 0-01111111100-1001100110011001100110011001100110011001100110011010;
相加得到 0-01111111101-0011001100110011001100110011001100110011001100110100;转换为十进制即为 0.30000000000000004。
单精度 32 位的 偏移量是 Math.pow(2,8)/2 -1 == 127 ,双精度 64 位的偏移量是 Math.pow(2,11)/2 -1 == 1023
因为所有数的二进制科学计数都可以写成 1.xxxx,所以是固定的,取消默认 1,这样可以多出一位存储空间。
var o = new Object
o[Symbol.iterator] = function() {
var v = 0
return {
next: function() {
return { value: v++, done: v > 10 }
}
}
};
for(var v of o)
console.log(v); // 0 1 2 3 ... 9
for of 可以遍历可迭代对象,new Object 是没有迭代器的,想要使用for of 必须使用 Symbol.iterator 添加迭代器。
一般对象遍历用 for in , 数组遍历用 for of。Array,Map,Set,String,TypedArray,arguments 等等对象默认为可迭代对象。
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
const a = {}
let b = a
a.c = 666
console.log(b.c) // 666
因为b和a指向同一个内存指针。同时也说明了const常量是针对内存指针的常量,只是内存指针地址不能改变。
先说几个概念
$router 表示整个路由对象,就是 new 的 VueRouter
$route 表示当前路由,就是在 routes 里配置的单个对象
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
params 是以/拼接url的,而且如果你写了 path 的话,params 是无效的。
query 是以问号的形式带在 url 上的参数
但是这样做了以后,后台就不会再返回 404 页面了,所以在前端覆盖了所有路由的情况下,路由里需要再添加一个404页面。
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})
// router
export default new Router({
mode: 'history',
routes: [
{ path: '/', component: Hello }, // No props, no nothing
{ path: '/hello/:name', component: Hello, props: true },
// 布尔模式: props 被设置为 true,此时route.params (即此处的name)将会被设置为组件属性。
{ path: '/static', component: Hello, props: { name: 'world', test: 'zhl' } },
// 对象模式: 此时就和 params 没什么关系了.此时的name将直接传给Hello组件.注意:此时的props需为静态!
{ path: '/dynamic/:years', component: Hello, props: dynamicPropsFn },
// 函数模式: 1,这个函数可以默认接受一个参数即当前路由对象.2,这个函数返回的是一个对象.3,在这个函数里你可以将静态值与路由相关值进行处理.
{ path: '/attrs', component: Hello, props: { name: 'attrs' } }
]
})
function dynamicPropsFn(route) {
const now = new Date()
return {
name: now.getFullYear() + parseInt(route.params.years) + '!'
}
}
// compontents 上面的Hello
<template>
<div>
<h2 class="hello">Hello {{name}} ,{{ $attrs }} ,{{$props}},{{test}}</h2>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
default: 'Vue!'
},
test: {
type: String,
default: ''
}
}
}
</script>
// vue
<div id="app">
<h1>Route props</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/hello/you">/hello/you</router-link></li>
<li><router-link to="/static">/static</router-link></li>
<li><router-link to="/dynamic/1">/dynamic/1</router-link></li>
<li><router-link to="/attrs">/attrs</router-link></li>
</ul>
<router-view class="view" foo="123"></router-view>
</div>
“导航”表示路由正在发生改变。
参数或查询的改变并不会触发进入/离开的导航守卫。
导航守卫分为全局的, 单个路由独享的, 或者组件级的。
一般登录的时候会用全局前置守卫来验证
// 大概的伪代码
router.beforeEach((to, from, next)=>{
//路由中设置的 route 信息就在to当中
if(window.sessionStorage.data){
// 查看是否是登录状态,一般会缓存token
if(to.path === '/'){
// 这里做逻辑处理,比如权限之类的
next({path: '/index'});
}else{
next();
}
}else{
// 如果没有session ,访问任何页面。都会进入到 登录页
if (to.path === '/') { // 如果是登录页面的话,直接next()
next();
} else { // 否则 跳转到登录页面
next({ path: '/' });
}
}
})
写在路由 meta 里的属性,可以利用这个来匹配白名单或者更改title之类的操作.
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
if (whiteList.indexOf(to.path) !== -1) {
// 页面在免登录白名单,直接进入,
// 这里的白名单是用导航守卫判断的,和 meta 没关系
if (to.meta.title1) {
// 如果是表单大师更改document title
document.title = to.meta.title1
}
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
需要注意的是,如果是嵌套路由,那需要遍历 $route.matched 来获取 meta 信息.
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
切换路由时,可以使页面滚动到你想要的某个地方,或者是保持之前滚动的位置,这时你就需要使用scrollBehavior这个方法.
const router = new VueRouter({
mode:'history',//这个不能忘,默认是hash模式
routes: [...],
scrollBehavior (to, from, savedPosition) {
// to:要进入的目标路由对象,到哪里去.和导航守卫的beforeEach一样
//from:离开的路由对象,哪里来
//savedPosition: 点击前进/后退的时候记录值{x:?,y:?}.并且只有通过浏览器的前进后退才会触发.
// return 期望滚动到哪个的位置 { x: number, y: number }或者是{ selector: string, offset? : { x: number, y: number }},这里selector接收字符串形式的hash,如'#foo',同时你还可以通过offset设置偏移,版本需要大于2.6+
//举个实例
if(savePosition) { //如果是浏览器的前进后退就,返回之前保存的位置
return savePosition;
}else if(to.hash) {//如果存在hash,就滚动到hash所在位置
return {selector: to.hash}
}else{
return {x:0,y:0}//否则就滚动到顶部
}
}
})
这个和组件的一样.按组件的写就行.
// 全部路由
<transition>
<router-view></router-view>
</transition>
单个路由
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}
// 动态
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}
可以动态往 $router 里添加 $route
router.addRoutes(routes: Array)
动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。
在日常开发中,经常会有一个按钮防止二次点击操作的需求。比如支付订单,创建商品,点赞收藏之类的。虽然不知道用户为什么要在0.5秒内连续点击两次甚至三次。。。
这些需求都可以通过函数防抖来实现。
函数防抖简单来说就是,触发高频事件后n秒内只执行一次,如果在时间内再次触发,那重新计算时间
function debounce(fn) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, 1000)
}
}
function sayHi(params) {
console.log('Hi');
}
let ObjDIV = document.getElementById('div')
ObjDIV.addEventListener('click', debounce(sayHi))
上面的代码在1s内连续点击的话,只会触发一次 sayHi ,而且1s的等待时间会从最后一次点击重置。
但是上面的代码有个缺陷, sayHi 只能在等待时间结束后调用,实际上一些需求是不可能这样做的。比如点赞,不可能要等1s之后才有反馈。所以我们将防抖函数进行下优化。
function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔last小于设定时间间隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
这样函数就会立即执行,并且在接下来的 wait 里不会重复执行,直到 wait 时间之后再次点击才可以执行下一次。
不带立即执行的应用场景
在这个 wait 时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
节流的一些应用场景:
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
在vue里怎么写?
import { debounce } from "@/utils/index";
onCreateSubmit: debounce(
function() {
createIntegralGoods(this.form).then(res => {
this.$message.success("商品创建成功");
this.$router.push({
path: "/campaign/active/integral-mall/index"
});
});
},
1000,
true
)
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);
typeof可以判断除了null的所有原始类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
但是不能判断对象,除了函数都会显示object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
instanceof 内部是通过原型链来判断的
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
// 没有.运算符并不会做装箱转换
var str1 = new String('hello world')
str1 instanceof String // true
instanceof 的判定如果在两个环境下可能会出错。比如网页内嵌 iframe。
而且 instanceof 的行为是可以自定义修改的。
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
在 JavaScript 中,没有任何方法可以更改固有对象的私有Class属性(自己创建的对象可以用symbol.toStringTag修改,如果不改默认[Object Object]),因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。
在早期JS版本中,“类”的定义是一个私有属性 [[class]]。内置类型可以使用Object.prototype.toString来访问[[class]]属性。
var o = new Object;
var n = new Number;
var s = new String;
var b = new Boolean;
var d = new Date;
var arg = function(){ return arguments }();
var r = new RegExp;
var f = new Function;
var arr = new Array;
var e = new Error;
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
在ES5之后,一些对象的[[class]]属性可以用Symbol.toStringTag来自定义。
var o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + "");
// 加法字符串,对象拆箱会先调用Object.prototype.toString
// 结果返回 [object MyObject]
console.dir(o) // Symbol(Symbol.toStringTag): "MyObject"
class ValidatorClass {
get [Symbol.toStringTag]() {
return "Validator";
}
}
// 也可以这样自定义类型标签
Symbol.toStringTag 只能更改自已创建的类,如果自己的类没有定义Symbol.toStringTag,toString会返回[object Object]
class ValidatorClass {}
Object.prototype.toString.call(new ValidatorClass()); // "[object Object]"
大多数内置对象即使没有toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签
Object.prototype.toString.call('foo'); // "[object String]"
Object.prototype.toString.call([1, 2]); // "[object Array]"
Object.prototype.toString.call(3); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
另外一些对象类型则不然,toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签:
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
ES6 模块的设计**,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
服务器端的 Node.js 遵循 CommonJS规范,该规范的核心**是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。
module、exports、require、global
所以用module.exports定义当前模块对外输出的接口(不推荐直接用exports)
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。
// 定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
ES6 在语言标准的层面上,实现了模块功能。其模块功能主要由两个命令构成:==export==和==import==。
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。
其实ES6还提供了==export default==命令,为模块指定默认输出,对应的import语句不需要使用大括号。
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
当浏览器报如下错误时,则说明请求跨域了。
localhost/:1 Failed to load http://www.thenewstep.cn/test/testToken.php: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
因为 浏览器
同源策略的限制,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要 安全机制
。
如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。
http://store.company.com/dir/page.html
http://store.company.com/dir2/other.html // 路径不同,同源
http://store.company.com/dir/inner/another.html // 路径不同,同源
https://store.company.com/secure.html // 协议不同,不是同源
http://store.company.com:81/dir/etc.html // 端口不同,不是同源
http://news.company.com/dir/other.html // 域名不同,不是同源
知道了跨域是因为浏览器的安全策略造成的,所以我们应该来消除对浏览器的误解
了解就行,现代工程化项目基本不会用到
在HTML标签里,一些标签比如script、img这样的获取资源的标签是没有跨域限制的。
比如,有个a.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://damonare.cn/data.php,那么a.html中的代码就可以这样:
<script type="text/javascript">
function dosomething(jsondata){
//处理获得的json数据
}
</script>
<script src="http://example.com/data.php?callback=dosomething"></script>
因为是当做一个js文件来引入的,所以http://damonare.cn/data.php返回的必须是一个能执行的js文件,这需要和后端约定好。
使用jQuery封装的JSONP方法可以很方便的进行jsonp请求:
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
//处理获得的json数据
});
</script>
jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。
想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?
早前一般是用Nginx做代理的。
server{
# 监听9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
现在工程化一般都在脚手架里集成了相关插件。
比如vue-cli里就默认加载了http-proxy-middleware依赖,名字也很直观http中间件代理...
设置的时候在config -> index.js -> 找到proxyTable
proxyTable: {
'/apis': {
// 测试环境
target: 'http://www.thenewstep.cn/', // 接口域名
changeOrigin: true, //是否跨域
pathRewrite: {
'^/apis': '' //需要rewrite重写的,
}
}
target:就是需要请求地址的接口域名
比如请求的地址是 http://www.thenewstep.cn/test/testToken.php
在axios里设置
import Vue from 'vue'
import App from './App'
import axios from 'axios'
Vue.config.productionTip = false
Vue.prototype.$axios = axios //将axios挂载在Vue实例原型上
// 设置axios请求的token
axios.defaults.headers.common['token'] = 'f4c902c9ae5a2a9d8f84868ad064e706'
//设置请求头
axios.defaults.headers.post["Content-type"] = "application/json"
axios请求页面代码
this.$axios.post('/apis/test/testToken.php',data).then(res=>{
console.log(res)
})
需要注意的是设置了proxyTable,axios就不能写baseURL了
还有就是proxyTable只在dev环境下有效。
CORS是一个W3C标准,CORS有两种请求,简单请求和非简单请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
如果是简单请求,浏览器会直接通过。
如果是非简单请求,浏览器会发出一次预检测请求。也就是Request Method为OPTIONS的请求。
后端会根据发起的请求头和请求方法来判断是否是之前约定的请求。如果是,浏览器才会发起真正的非简单请求。如果不是,会报跨域错误。
比如前端请求头加了一个test字段,但是后端没有设置允许该请求头就会在预请求时报错,不会发起真正的请求。
简单说,CORS就是前端请求的头和方法是后端CORS设置允许的就可以。
以下的了解就行
判断是否属于简单请求,只要同时满足以下两大条件
(1)请求方法是以下三种方法之一:
(2)HTTP的头信息不超出以下几种字段:
有关 CORS 预请求 response Headers 的解释
Access-Control-Allow-Credentials: true
// 表示是否可以将对请求的响应暴露给页面。返回true则可以,其他值均不可以。
Access-Control-Allow-Headers: Content-Type, X-REST-CORS, X-Auth-Token,data,ak,sign,time,token
// 在正式请求中可以使用的请求头,如果请求头中使用了没有在表中的请求头就会报错
Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS
// 规定了所要访问的资源允许使用的方法。如果使用了不在列表的请求方式会报错
Access-Control-Allow-Origin: *
// 规定了资源可以被请求的域,“*”表示所有域都可以访问,但要符合上面的其他规定
Access-Control-Max-Age: 86400
// 规定了这个资源预请求的缓存时间,在时间内再次发起请求不会发预请求,会直接发正式请求拿到数据。
JS里的类型转换有四种情况,原始类型转Object的一般不用手动处理,默认的装箱转换会自动处理.
原始值 | 转化为Boolean | 转化为Number | 转化为String | 转化为Object |
---|---|---|---|---|
Null | false | 0 | "null" | TypeError |
Undefined | false | NaN | "undefined" | TypeError |
Boolean(true) | - | 1 | "true" | 装箱转换 |
Boolean(false) | - | 0 | "false" | 装箱转换 |
Number | 除了0/-0/NaN都为true | - | #NumberToString | 装箱转换 |
String | ""为flase | #StringToNumber | - | 装箱转换 |
Symbol | true | TypeError | TypeError | 装箱转换 |
Object | true | 拆箱转换 | 拆箱转换 | - |
装箱拆箱看第8天
关于null、undefined转换为Object,在权威指南中写的是TypeError,但这里其实用Object()来转换时,会返回一个空对象{}。但这个空对象是没有原始值 [[PrimitiveValue]] 的,只是一个空对象。
Object拆箱转换补充
原始值 | 转化为Boolean | 转化为Number | 转化为String |
---|---|---|---|
{}任意对象 | true | NaN | toString() |
[] | [null] | ||
[8]单数字数组 | true | 9 | "9" |
["a",4,true] | true | NaN | "a,4,true" |
function(){} | true | NaN | "function(){}" |
不涉及运算符的情况下,上面两个表就够用了。但实际上隐式转换都是伴随着运算符一起 出现的。
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
加法运算符是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。由于加法运算符存在重载,可能执行两种运算,使用的时候必须很小心。
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
console.log('a' + - 'b') // aNaN
对于除了加法的运算符(比如减法、除法和乘法)来说,都不会发生重载,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
JavaScript 一共提供了8个比较运算符。
这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。
如果两个运算子都是原始类型的值,则是先转成数值再比较。
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4
true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 0
2 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1
任何值(包括NaN本身)与NaN比较,返回的都是false。
1 > NaN // false
1 <= NaN // false
'1' > NaN // false
'1' <= NaN // false
NaN > NaN // false
NaN <= NaN // false
如果运算子是对象,会转为原始类型的值,再进行比较。
对象拆箱部分看第8天
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
let a = {
valueOf() {
return {}
},
toString() {
return {}
}
}
a > -1 // TypeError
let a = {}
a > 1 // false
let a = {}
a + 1 // [object Object]1
严格相等
原始类型,值和类型全部相同,返回true,否则返回false
1 === 0x1 // true
true === "true" // false
NaN === NaN // false
+0 === -0 // true
复合类型,两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。
{} === {} // false
[] === [] // false
(function () {} === function () {}) // false
如果两个变量引用同一个对象
var v1 = {};
var v2 = v1;
v1 === v2 // true
对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。
var obj1 = {};
var obj2 = {};
obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false
undefined和null与自身严格相等,所以
var v1;
var v2;
v1 === v2 // true
相等
对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换。
假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程:
如果 x 或 y 中有一个为 NaN,则返回 false;
如果 x 与 y 皆为 null 或 undefined 中的一种类型,则返回 true(null == undefined // true);否则返回 false(null == 0 // false);
如果 x,y 类型不一致,且 x,y 为 String、Number、Boolean 中的某一类型,则将 x,y 使用 toNumber 函数转化为 Number 类型再进行比较;
如果 x,y 中有一个为 Object,则首先使用 ToPrimitive 函数将其转化为原始类型,再进行比较。
任何值(包括NaN本身)与NaN比较,返回的都是false。
1 == NaN // false
'1' == NaN // false
NaN == NaN // false
[] == NaN // false
{} == NaN // false
false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true
1 == true // true
// 等同于 1 === Number(true)
0 == false // true
// 等同于 0 === Number(false)
2 == true // false
// 等同于 2 === Number(true)
2 == false // false
// 等同于 2 === Number(false)
'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1
'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0
'' == false // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0
'1' == true // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1
'\n 123 \t' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空格
// 对象与数值比较时,对象转为数值
[1] == 1 // true
// 等同于 Number([1]) == 1
// 对象与字符串比较时,对象转为字符串
[1] == '1' // true
// 等同于 String([1]) == '1'
[1, 2] == '1,2' // true
// 等同于 String([1, 2]) == '1,2'
// 对象与布尔值比较时,两边都转为数值
[1] == true // true
// 等同于 Number([1]) == Number(true)
[2] == true // false
// 等同于 Number([2]) == Number(true)
最后来几个题看一下
[] == ![]
// 首先 [] 为对象,则调用 ToPrimitive 函数将其转化为字符串 "";
// 对于右侧的 ![],首先会进行显式类型转换,将其转化为 false。
// 然后在比较运算中,会将运算符两侧的运算对象都转化为数值类型,即都转化为了 0,因此最终的比较结果为 true。
['0'] == false ??
[null] == false ??
[] + {} ??
1 + {a:1} ??
原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString() 会抛出错误
但为什么 '1'.toString() 是可以使用的。
其实是 . 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。在该临时对象调用函数返回函数操作结果后,将该对象丢弃。
每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。
var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObject); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor Symbol); // true
使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。
var symbolObject = Object((Symbol("a"));
console.log(typeof symbolObject); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor Symbol); // true
每一个装箱对象 console.dir 的时候,会发现有个 [[PrimitiveValue]] 标记,他会显示该对象内部指向的原始值。
每一类对象(包括装箱对象)皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:
var symbolObject = Object((Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。
instanceof 的判定如果在两个环境下可能会出错。比如网页内嵌 iframe。
但需要注意的是,call 本身会产生装箱操作,所以判断类型的时候需要配合 typeof 来区分基本类型还是对象类型。
在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。
[Symbol.toPrimitive](hint)
如果 hint 是 "string" 或 "default",@@toPrimitive 将会调用 toString。如果 toString 属性不存在,则调用 valueOf。如果 valueOf 也不存在,则抛出一个 TypeError。
如果 hint 是 "number",@@toPrimitive 会首先尝试 valueOf,若失败再尝试 toString。
number
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// Uncaught TypeError: Cannot convert object to primitive value
string
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o + ""
// toString
// valueOf
// Uncaught TypeError: Cannot convert object to primitive value
当在希望是字符串操作,也即发生对象到字符串的转换时,传入内部函数 ToPrimitive 的参数值即为 string,当在希望是数值操作,传入内部函数 ToPrimitive 的参数值即为 number,当在一些不确定需要将对象转换成什么基础类型的场景下,传入内部函数 ToPrimitive 的参数值即为 default:
const b = {
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`)
return {}
},
toString() {
console.log('toString')
return 1
},
valueOf() {
console.log('valueOf')
return 2
}
}
alert(b) // hint: string
b + '' // hint: default
b + 500 // hint: default
;+b // hint: number
b * 1 // hint: number
在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello
// 随便写几个例子
var o = { [Symbol.toStringTag]: 'MyObject' }
console.log(o + ''); // [object MyObject]
// + '' hint 为 string 调用 toString 方法, [Symbol.toStringTag] 改变了 [Object Object]
console.log( o * 2 ) // NaN
// hint 为 number, 调用 valueOf 方法, 返回对象本身, 再调用 toString方法. 返回字符串'[object MyObject ]'
// '[object MyObject ]' 转 Number 为 NaN, NaN * 2 还是 NaN
let fn = function test(params) {
return 1
}
let object = {}
let arr = [
123,
123,
'string',
'string',
true,
true,
undefined,
undefined,
null,
null,
object,
object,
NaN,
NaN,
{},
{},
fn,
fn
]
// 1. es6 set
console.log([...new Set(arr)])
// [123, "string", true, undefined, null, {…}, NaN, {…}, {…}, ƒ(test)]
// Set是ES6里一种新的数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。
// 扩展运算符(...)能将一个数组转为用逗号分隔的参数序列。
// 扩展运算符(...)内部使用for...of循环,所以自定义对象是不能使用 (...),当然手动添加迭代器之后可以使用。
// 在Set里,同一引用对象是相等的,不同引用的对象不相等。
或者用Array.from
console.log(Array.from(new Set(arr)))
// Array.from 可以把类数组转成数组
// 类数组和数组都有length属性和非负整数下标,不同的是类数组没有数组方法。
[...new Set('ababbc')].join('')
// "abc"
// 上面的方法也可以用于,去除字符串里面的重复字符。
// 2. 暴力双层循环
function unique(array) {
// res用来存储结果
var res = []
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < res.length; j++) {
if (array[i] === res[j]) {
break
}
}
if (j === res.length) {
res.push(array[i])
}
}
return res
}
// [123, "string", true, undefined, null, {…}, NaN, NaN, {…}, {…}, ƒ(test)]
// 因为是用 === 判断的,所以NaN不会去重。对象同一引用会去重,不同引用不去重。
3. indexOf
function unique(array) {
var res = []
for (var i = 0, len = array.length; i < len; i++) {
var current = array[i]
if (res.indexOf(current) === -1) {
res.push(current)
}
}
return res
}
// [123, "string", true, undefined, null, {…}, NaN, NaN, {…}, {…}, ƒ(test)]
// 结果和 === 一样
// 利用filter的特性返回一个符合条件的数组
function unique(array) {
var res = array.filter(function(item, index, array){
return array.indexOf(item) === index;
})
return res;
}
// [123, "string", true, undefined, null, {…}, {…}, {…}, ƒ(test)]
// 4. 利用Object键值对
function unique(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
//[ 123, "string", true, undefined, null, {…}, NaN, ƒ(test)]
// 这个会去重所有。包括不是同一引用但是表现相同的对象。
// filter 会返回符合表达式的数组,创建一个空对象obj。如果obj 没有自身属性 typeof item + item,那把typeof item + item添加为obj 的 [key],[value]为true,并且为真返回到filter的数组里。
//下一个,如果有[key]为typeof item + item,说明有重复的,返回false并且不返回到filter的数组。
需要知道的前置知识,原始类型和引用类型的区别。
知道了引用类型的特点,就知道对象的拷贝是会相互影响的。但是实际业务中,有时需要将一个引用类型拷贝一份,并且两个对象的值还不会相互影响。
这里存在两种情况:浅拷贝和深拷贝
let object1 = {
a: 1,
obj: {
b: 'string'
}
}
浅拷贝也存在两种情况:
// 最简单的浅拷贝
let object2 = object1; // 两个对象指向一个引用地址,改一个另一个也会改变
let object2 = Object.assign({}, object1)
object1.a = 666
object1.obj.b = 'newString'
console.log( object2.a ) // 1
console.log( object1.a ) // 666
console.log( object2.obj.b ) // 'newString'
浅拷贝存在许多问题,需要我们注意:
function Parent() {
this.name = 'parent'
this.a = 1
}
function Child() {
this.name = 'child'
this.b = 2
}
Child.prototype = new Parent()
let child1 = new Child()
console.log(child1.a, child1.name); // 1 "child"
console.log(Parent.prototype);
console.log(Child.prototype);
Object.defineProperty(child1, 'name', {writable: false, value: 'ARRON'})
// 更改child1的描述符writable为false
let child2 = Object.assign({}, child1)
console.log(Object.getOwnPropertyDescriptor(child2, 'name'));
// Object{value: "ARRON", writable: true, enumerable: true, configurable: true}
// 这里描述符的可赋值writable已经变成true
child1.name = 'newName'; // 严格模式下报错,普通模式下无效
child2.name = 'newName'; // 可以赋值
console.log( child1.name ); // ARRON
console.log( child2.name ); // newName
// 查看 child1 和 child2 的原型,我们也会发现它们的原型也是不同的
console.log(child1.__proto__); // Parent
console.log(child2.__proto__); // Object
es6之后可以用Object.getPrototypeOf()来访问非标准但许多浏览器实现的属性 __proto__
深拷贝就是将对象的属性递归的拷贝到一个新的对象上,两个对象有不同的地址,不同的引用,也包括对象里的对象属性(如 object1 中的 obj 属性),两个变量之间完全独立。
var object2 = Object.assign({}, object1);
Object.getOwnPropertyNames() 返回由对象属性组成的一个数组,包括不可枚举的属性(除了使用 Symbol 的属性)。
function shallowCopyOwnProperties(source) {
var target = {}
var keys = Object.getOwnPropertyNames(original)
for (var i = 0; i < keys.length; i++) {
target[keys[i]] = source[keys[i]]
}
return target
}
function shallowCopy(source) {
// 用 source 的原型创建一个对象
var target = Object.create(Object.getPrototypeOf(source))
// 获取对象的所有属性
var keys = Object.getOwnPropertyNames(source)
// 循环拷贝对象的所有属性
for (var i = 0; i < keys.length; i++) {
// 用原属性的描述符创建新的属性
Object.defineProperty(target, keys[i], Object.getOwnPropertyDescriptor(source, keys[i]))
}
return target
}
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// 直接复制
var array1 = array;
// 遍历直接复制
var array2 = [];
for(var key in array) {
array2[key] = array[key];
}
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // newString
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// slice()
var array1 = array.slice();
// concat()
var array2 = array.concat();
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // string
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
var obj = { a: 1, b: { c: 2 }};
// 深拷贝
var newObj = JSON.parse(JSON.stringify(obj));
// 改变原对象的属性
obj.b.c = 20;
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }
优点是方便简洁,可以处理大多数业务需求。
缺点是属性里有function、undefined和symbol的话会被忽略。并且如果值有循环引用对象的话会报错。
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // 循环引用会报错
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
手写或者用 lodash 的深拷贝函数
思考题:为什么vue里会有深度监听,$set和数组变异方法?
提示可以参考查看第二天双向数据绑定demo的 observer 函数.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.