前端开发 大前端 W3Cbest

一个专注 WEB 开发的技术博客

0%

String 类型用于表示由零或多个16位 Unicode 字符组成的字符序列,即字符串 。

存储结构

由于计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。在计算机中,1个字节(byte)由 8个比特(bit)组成,所以 1 个字节能表示的最大整数就是 255,如果想表示更大整数,就必须用更多的字节,比如 2 个字节可以表示的最大整数为 65535 。最早,只有127个字符被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122。 但如果要表示中文字符,显然一个字节是不够的,至少需要两个字节。所以,中国制定了GB2312编码,用来表示中文字符。基于同样的原因,各个国家都制定了自己的编码规则。这样就会出现一个问题,即在多语言混合的文本中,不同的编码会出现冲突,导致乱码出现。 为了解决这个问题,Unicode 编码应运而生,它把所有的语言都统一到一套编码中,采用2个字节表示一个字符,即最多可以表示65535个字符,这样基本上可以覆盖世界上常用的文字,如果要表示更多的文字,也可以采用4个字节进行编码,这是一种通用的编码规范。 因此,JavaScript中的字符也采用Unicode来编码,也就是说,JavaScript中的英文字符和中文字符都会占用2个字节的空间大小,这种多字节字符,通常被称为宽字符。

基本包装类型

在JavaScript中,字符串是基本数据类型,本身不存任何操作方法。为了方便的对字符串进行操作,ECMAScript提供了一个基本包装类型:String()对象。它是一种特殊的引用类型,JS引擎每当读取一个字符串的时候,就会在内部创建一个对应的String()对象,该对象提供了很多操作字符的方法,这就是为什么能对字符串调用方法的原因。

var name = ‘JavaScript’;
var value = name.substr(2,1);

当第二行代码访问变量str时,访问过程处于一种读取模式,也就是要从内存中读取这个字符串的值。而在读取模式中访问字符串时,引擎内部会自动完成下列处理:

  1. 创建String类型的一个实例
  2. 在实例上调用指定的方法
  3. 销毁这个实例

用伪代码形象的模拟以上三个步骤:

var obj = new String(‘JavaScript’);
var value = obj.substr(2,1);
name = null;

可以看出,基本包装类型是一种特殊的引用类型。它和普通引用类型有一个很重要的区别,就是对象的生存期不同。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。在JavaScript中,类似的基本包装类型还有NumberBoolean对象 。

常用操作方法

作为字符串的基本包装类型,String()对象提供了以下几类方法,用以操作字符串:

  • 字符操作:charAt()charCodeAt()fromCharCode()
  • 字符串提取:substr()substring()slice()
  • 位置索引:indexOf()lastIndexOf()
  • 大小写转换:toLowerCase()toUpperCase()
  • 模式匹配:match()search()replace()split()
  • 其他操作:concat()trim()localeCompare()

charCodeAt()方法的作用是获取字符的Unicode编码,俗称“Unicode码点”。fromCharCode()String()对象上的静态方法,作用是根据Unicode编码返回对应的字符。

var a = ‘a’;
// 获取Unicode编码
var code = a.charCodeAt(0); // 97
// 根据Unicode编码获取字符
String.fromCharCode(code); // a

通过charCodeAt()方法获取字符的Unicode编码,然后再把这个编码转化成二进制,就可以得到该字符的二进制表示。

var a = ‘a’;
var code = a.charCodeAt(0); // 97
code.toString(2); // 1100001

对于字符串的提取操作,有三个相类似的方法,分别如下:

substr(start [, length])
substring(start [, end])
slice(start [, end])

从定义上看,substring()slice()是同类的,参数都是字符串的某个start位置到某个end位置(但end位置的字符不包括在结果中);而substr()则是字符串的某个start位置起,数length个长度的字符才结束。二者的共性是:从start开始,如果没有第2个参数,都是直到字符串末尾。 substring()slice()的区别则是:slice()可以接受“负数”,表示从字符串尾部开始计数;而substring()则把负数或其它无效的数当作0。

‘hello world!’.slice(-6, -1) // ‘world’
‘hello world!’.substring(“abc”, 5) // ‘hello’

substr()的start也可接受负数,也表示从字符串尾部计数,这点和slice()相同;但substr()的length则不能小于1,否则返回空字符串。

‘hello world!’.substr(-6, 5) // ‘world’
‘hello world!’.substr(0, -1) // ‘’

说明:正则表达式通常用于两种任务:1.验证,2.搜索/替换。用于验证时,通常需要在前后分别加上^$,以匹配整个待验证字符串;搜索/替换时是否加上此限定则根据搜索的要求而定,此外,也有可能要在前后加上\b而不是^$。此表所列的常用正则表达式,除个别外均未在前后加上任何限定,请根据需要,自行处理。

说明

正则表达式

网址(URL)

[a-zA-z]+://[^\s]*

IP地址(IP Address)

((2[0-4]\d25[0-5][01]?\d\d?)\.){3}(2[0-4]\d25[0-5][01]?\d\d?)

电子邮件(Email)

\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

QQ号码

[1-9]\d{4,}

HTML标记(包含内容或自闭合)

<(.*)(.*)>.*<\/\1><(.*) \/>

密码(由数字/大写字母/小写字母/标点符号组成,四种都必有,8位以上)

(?=^.{8,}$)(?=.*\d)(?=.*\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\n).*$

日期(年-月-日)

(\d{4}\d{2})-((1[0-2])(0?[1-9]))-(([12][0-9])(3[01])(0?[1-9]))

日期(月/日/年)

((1[0-2])(0?[1-9]))/(([12][0-9])(3[01])(0?[1-9]))/(\d{4}\d{2})

时间(小时:分钟, 24小时制)

((10?)[0-9]2[0-3]):([0-5][0-9])

汉字(字符)

[\u4e00-\u9fa5]

中文及全角标点符号(字符)

[\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee]

中国大陆固定电话号码

(\d{4}-\d{3}-)?(\d{8}\d{7})

中国大陆手机号码

1\d{10}

中国大陆邮政编码

[1-9]\d{5}

中国大陆身份证号(15位或18位)

\d{15}(\d\d[0-9xX])?

非负整数(正整数或零)

\d+

正整数

[0-9]*[1-9][0-9]*

负整数

-[0-9]*[1-9][0-9]*

整数

-?\d+

小数

(-?\d+)(\.\d+)?

不包含abc的单词

\b((?!abc)\w)+\b

Javascript中最基本的迭代方法是for循环。它需要三个表达式:变量声明、在每次迭代之前要计算的表达式以及在每次迭代结束时要计算的表达式。例如,这个for循环将console.log数组中的每个项。

const array = [‘a’, ‘b’, ‘c’, ‘d’];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
// Result: a, b, c, d

除了for循环之外,我们还可以使用另外两种for迭代方法:for..infor..of

for..in

for..in是一种迭代对象的“可枚举”属性的方法。因此,它适用于所有具有这些属性的对象(不仅是object())。 可枚举属性定义为可枚举值为true的对象的属性。从本质上讲,如果一个属性是可枚举的,它就是“可枚举的”。我们可以通过调用property.Enumerable来检查属性是否可枚举,它将返回truefalse。 我们使用for..in循环与以下语法一起使用

for (variable in enumerable) {
// do stuff
}

例如,要循环遍历console.logObject中的所有值,我们可以执行以下操作 -

const obj = {
a: 1,
b: 2,
c: 3,
d: 4
}
for (const key in obj) {
console.log( obj[key] )
}

for…in循环也将迭代继承的属性,只要它们是可枚举的属性。for…in迭代以任意顺序发生。因此,如果需要按照定义的顺序发生,则不应使用它。

for..in 和 Object

for..in方法为我们提供了循环对象键和值的最简单方法,因为对象不能访问数组所使用的forEach方法。

for..in 和 Array

数组中值的“键”是数字索引。因此,这些索引本质上只是可枚举的属性,就像Object键一样,只是它们是整数而不是字符串。 这意味着我们可以通过使用for..in来检索数组中的索引来循环数组中的所有值。

const array = [‘a’, ‘b’, ‘c’, ‘d’];

for (const index in array) {
console.log(array[index])
}

// Result: a, b, c, d

但是,一般建议不要将for..in与数组一起使用,特别是因为不能保证迭代按顺序进行,这通常对数组很重要。

for..in 和 String

字符串中的每个字符都有一个索引。因此,与Array类似,索引是可枚举的属性,恰好是整数。

const string = ‘Ire Aderinokun’;

for (const index in string) {
console.log(string[index])
}

// Result: I, r, e, , A, d, e, r, i, n, o, k, u, n

for..of

for..of是在ES2015中引入的一种方法,用于迭代“iterable collections”。这些对象具有[symbol.iterator]属性。 [symbol.iterator]属性允许我们通过调用[symbol.iterator]().next()方法来检索集合中的下一项,从而手动迭代集合。

const array = [‘a’,’b’,’c’, ‘d’];
const iterator = array[Symbol.iterator]();
console.log( iterator.next().value )
console.log( iterator.next().value )
console.log( iterator.next().value )
console.log( iterator.next().value )

// Result: a, b, c, d

for..of语法实质上是围绕[symbol.iterator]的包装,以创建循环。它使用以下语法-

for (variable of iterable) {
// do stuff
}

for..of 和 Object

for..of循环不适用于对象,因为他们不是“迭代”,因此不具有[Symbol.iterator]属性。

for..of 和 Array/String

for..of循环使用数组和字符串的效果很好,因为它们是可迭代。此这种方法是一种更可靠的循环遍历数组的方法。

const array = [‘a’, ‘b’, ‘c’, ‘d’];
for (const item of array) {
console.log(item)
}
// Result: a, b, c, d

const string = ‘Ire Aderinokun’;
for (const character of string) {
console.log(character)
}
// Result: I, r, e, , A, d, e, r, i, n, o, k, u, n

for..of 和 NodeLists

最后,另一个非常有用的案例for..of是迭代NodeLists。当我们在文档中查询一组元素时,返回的是NodeList,而不是Array。这意味着我们不能使用forEach等数组方法对列表进行迭代。 为了解决这个问题,我们可以使用Array.from()或者使用for..of循环将其转换为数组,这不仅仅适用于数组。

const elements = document.querySelectorAll(‘.foo’);

for (const element of elements) {
element.addEventListener(‘click’, doSomething);
}

对比

for..in

for..of

适用于

可枚举属性

无法检索的集合

与Object一起使用?

Yes

No

与Array一起使用?

Yes, 不建议

Yes

与String一起使用?

Yes, 不建议

Yes

假设我们有一个对象数组,如下所示:

const books = [
{
name: “My Sister the Serial Killer”,
author: “Oyinkan Braithwaite”
},
{
name: “Educated”,
author: “Tara Westover”
},
{
name: “My Sister the Serial Killer”,
author: “Oyinkan Braithwaite”
}
];

数组中的第一个和最后一个对象是相同的。那么如果我们想从数组中删除这些重复的对象呢?令人惊讶的是,这是一个非常难以解决的问题。为了理解原因,让我们看一下如何从平面项目数组中删除重复项,例如字符串。

从数组中删除重复的项

假设我们有一个字符串数组,如下所示:

const strings = [
“My Sister the Serial Killer”,
“Educated”,
“My Sister the Serial Killer”
];

如果我们想从这个数组中删除任何重复项,我们可以使用该filter()方法以及indexOf()方法来检查任何给定项是否重复。

const filteredStrings = strings.filter((item, index) => {
// Return to new array if the index of the current item is the same
// as the first occurence of the item
return strings.indexOf(item) === index;
});

由于strings.indexOf(item)将始终返回第一次出现的索引item,我们可以判断过滤器循环中的当前项是否重复。如果是,我们不会将其返回到由该filter()方法创建的新数组

对象的工作方式不同

这个方法不适用于对象的原因是因为任何具有相同属性和值的2个对象实际上并不相同。

const a = {
name: “My Sister the Serial Killer”,
author: “Oyinkan Braithwaite”
};
const b = {
name: “My Sister the Serial Killer”,
author: “Oyinkan Braithwaite”
};

a === b // false

这是因为基于参考而不是结构来比较对象。在比较两个对象时,不考虑两个对象具有相同的orperties和value的事实。因此,即使存在具有完全相同属性和值的另一个对象indexOf(object),对象数组内也将始终返回精确object传递的索引。

解决方案

给定此信息,检查两个对象是否具有相同属性和值的唯一方法是实际检查每个对象的属性和值。我提出的解决方案涉及进行此手动检查,但有一些改进性能并减少不必要的嵌套循环。 需要注意3点:

  1. 仅检查数组中的每个项目与其后的每个其他项目,以避免多次比较相同的对象
  2. 仅检查未找到与任何其他项目重复的项目
  3. 在检查每个属性的值是否相同之前,请检查两个对象是否具有相同的键

这是最后的功能:

function removeDuplicates(arr) {

const result = \[\];
const duplicatesIndices = \[\];

// Loop through each item in the original array
arr.forEach((current, index) => {

    if (duplicatesIndices.includes(index)) return;

    result.push(current);

    // Loop through each other item on array after the current one
    for (let comparisonIndex = index + 1; comparisonIndex < arr.length; comparisonIndex++) {
    
        const comparison = arr\[comparisonIndex\];
        const currentKeys = Object.keys(current);
        const comparisonKeys = Object.keys(comparison);
        
        // Check number of keys in objects
        if (currentKeys.length !== comparisonKeys.length) continue;
        
        // Check key names
        const currentKeysString = currentKeys.sort().join("").toLowerCase();
        const comparisonKeysString = comparisonKeys.sort().join("").toLowerCase();
        if (currentKeysString !== comparisonKeysString) continue;
        
        // Check values
        let valuesEqual = true;
        for (let i = 0; i < currentKeys.length; i++) {
            const key = currentKeys\[i\];
            if ( current\[key\] !== comparison\[key\] ) {
                valuesEqual = false;
                break;
            }
        }
        if (valuesEqual) duplicatesIndices.push(comparisonIndex);
        
    } // end for loop

}); // end arr.forEach()
return result;

}

单行简洁的代码很难维护(有时甚至难以理解),但这并不能阻止广大攻城狮们脑洞,在编写简洁的代码后获得一定的满足感。 以下我最近的一些收藏javascript精简代码集合。它们都可以在你的开发控制台中运行,你可以从控制台中查看运行结果。同时,我希望你能在评论中分享一些自己的藏品

日历

创建过去七天的数组,如果将代码中的减号换成加号,你将得到未来7天的数组集合

// 创建过去七天的数组
[…Array(7).keys()].map(days => new Date(Date.now() - 86400000 * days));

Array.from(Array(7).keys(),(days) => new Date(Date.now() - 86400000 * days));

Array.from({length:7},(_,days) => new Date(Date.now() - 86400000 * days));

生成随机ID

在原型设计时经常使用的创建ID功能。但是我在实际项目中看到有人使用它。其实这并不安全

// 生成长度为11的随机字母数字字符串
Math.random().toString(36).substring(2);

取URL的查询参数

这个获取URL的查询参数代码,是我见过最精简的QAQ ?foo=bar&baz=bing => {foo: bar, baz: bing}

// 获取URL的查询参数
q={};location.search.replace(/([^?&=]+)=([^&]+)/g,(_,k,v)=>q[k]=v);q;

Object.fromEntries(new URLSearchParams(‘a=1&b=2’))

本地时间

通过一堆HTML,您可以创建一个本地时间,其中包含您可以一口气读出的源代码,它每秒都会用当前时间更新页面

// 创建本地时间

new Date().toLocaleTimeString()

数组混淆

随机更改数组元素顺序,混淆数组

// 随机更改数组元素顺序,混淆数组
(arr) => arr.slice().sort(() => Math.random() - 0.5)
/*
let a = (arr) => arr.slice().sort(() => Math.random() - 0.5)
let b = a([1,2,3,4,5])
console.log(b)
*/

或者

function shuffle(arr) {
const newArr = […arr];
for (let i = 0, len = newArr.length; i < len; i += 1) {
const random = Math.floor(Math.random() * (i + 1));
if (i !== random){
[newArr[i], newArr[random]] = [newArr[random], newArr[i]];
}
}
return newArr;
}

或者

function shuffle(arr) {
let i = arr.length, j;
while (i) {
j = Math.floor(Math.random() * i–);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

生成随机十六进制代码(生成随机颜色)

使用JavaScript简洁代码生成随机十六进制代码

// before
‘#’ + Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, ‘0’);

// after
‘#’ + (0x1000000 + Math.random() * 0xffffff).toString(16).slice(1, 6);

Array.from({ length: 6 }, () => Math.floor(Math.random() * 16).toString(16)).join(“”)

一个面试题

这是一个臭名昭著的面试题,让你写出他的运行结果

for (i = 0; ++i < 101; console.log(i % 5 ? f i : f + ‘Buzz’)) f = i % 3 ? ‘’ : ‘Fizz’

数组去重

这是一个原生的JS函数但是非常简洁,Set接受任何可迭代对象,如数组[1,2,3,3],并删除重复项

// 数组去重
[…new Set(arr)]
// 或者
arr.filter((x, index, self) => self.indexOf(x) === index)

创建特定大小的数组

方便快捷创建特定大小的数组

// 创建一个数组
[…Array(3).keys()]

或者

Array.from({length: 3}, (val, i) => i)

返回一个键盘

这是一个很难看懂的简洁代码,但是运行后你会惊呆的,他竟然返回一个图形键盘

// 用字符串返回一个键盘图形
(_=>[…”`1234567890-=QWERTYUIOP[]\\~ASDFGHJKL;’ZXCVBNM,./~”].map(x=>(o+=`/${b=’_‘.repeat(w=x<y?2:’ 667699’[x=[“BS”,”TAB”,”CAPS”,”ENTER”][p++]‘SHIFT’,p])}\\`,m+=y+(x+’ ‘).slice(0,w)+y+y,n+=y+b+y+y,l+=’ __‘+b)[73]&&(k.push(l,m,n,o),l=’’,m=n=o=y),m=n=o=y=’’,p=l=k=[])&&k.join`
`)()

参考文章来源:https://dev.to

对象是 JavaScript 的基本块。对象是属性的集合,属性是键值对。JavaScript 中的几乎所有对象都是位于原型链顶部 Object 的实例。

介绍

如你所知,赋值运算符不会创建一个对象的副本,它只分配一个引用,我们来看下面的代码:

let obj = {
a: 1,
b: 2,
};
let copy = obj;
obj.a = 5;
console.log(copy.a);
// Result
// a = 5;

obj 变量是一个新对象初始化的容器。copy 变量指向同一个对象,是对该对象的引用。所以现在有两种方式可以访问这个 { a: 1, b: 2, } 对象。你必须通过 obj 变量或 copy 变量,无论你是通过何种方式对这个对象进行的任何操作都会影响该对象。 不变性(Immutability)最近被广泛地谈论,这个很重要!上面示例的方法消除了任何形式的不变性,如果原始对象被你的代码的另一部分使用,则可能导致bug。

复制对象的原始方式

复制对象的原始方法是循环遍历原始对象,然后一个接一个地复制每个属性。我们来看看这段代码:

function copy(mainObj) {
let objCopy = {}; // objCopy will store a copy of the mainObj
let key;

for (key in mainObj) {
    objCopy\[key\] = mainObj\[key\]; // copies each property to the objCopy object
}
return objCopy;

}

const mainObj = {
a: 2,
b: 5,
c: {
x: 7,
y: 4,
},
}

console.log(copy(mainObj));

存在的问题

  1. objCopy 对象具有一个新的 Object.prototype方法,这与 mainObj 对象的原型方法不同,这不是我们想要的。我们需要精确的拷贝原始对象。
  2. 属性描述符不能被复制。值为 false 的 “可写(writable)” 描述符在 objCopy 对象中为 true 。
  3. 上面的代码只复制了 mainObj 的可枚举属性。
  4. 如果原始对象中的一个属性本身就是一个对象,那么副本和原始对象之间将共享这个对象,从而使其各自的属性指向同一个对象。

解决方法

当 writable 设置为false时,表示不可写,也就是说属性不能被修改。

var o = {}; // Creates a new object

Object.defineProperty(o, ‘a’, {
value: 37,
writable: false
});

console.log(o.a); // logs 37
o.a = 25; // No error thrown
// (it would throw in strict mode,
// even if the value had been the same)
console.log(o.a); // logs 37. The assignment didn’t work.

// strict mode
(function() {
‘use strict’;
var o = {};
Object.defineProperty(o, ‘b’, {
value: 2,
writable: false
});
o.b = 3; // throws TypeError: “b” is read-only
return o.b; // returns 2 without the line above
}());

正如上例中看到的,修改一个 non-writable 的属性不会改变属性的值,同时也不会报异常。

浅拷贝对象

当拷贝源对象的顶级属性被复制而没有任何引用,并且拷贝源对象存在一个值为对象的属性,被复制为一个引用时,那么我说这个对象被浅拷贝。如果拷贝源对象的属性值是对象的引用,则只将该引用值复制到目标对象。 浅层复制将复制顶级属性,但是嵌套对象将在原始(源)对象和副本(目标)对象之间是共享。

使用 Object.assign() 方法

Object.assign() 方法用于将从一个或多个源对象中的所有可枚举的属性值复制到目标对象。

let obj = {
a: 1,
b: 2,
};
let objCopy = Object.assign({}, obj);
console.log(objCopy);
// Result - { a: 1, b: 2 }

到目前为止。我们创建了一个 obj 的副本。让我们看看是否存在不变性:

let obj = {
a: 1,
b: 2,
};
let objCopy = Object.assign({}, obj);

console.log(objCopy); // result - { a: 1, b: 2 }
objCopy.b = 89;
console.log(objCopy); // result - { a: 1, b: 89 }
console.log(obj); // result - { a: 1, b: 2 }

在上面的代码中,我们将 objCopy 对象中的属性 b 的值更改为 89 ,并且当我们在控制台中 log 修改后的 objCopy 对象时,这些更改仅应用于 objCopy 。我们可以看到最后一行代码检查 obj 对象并没有被修改。这意味着我们已经成功地创建了拷贝源对象的副本,而且它没有引用。

Object.assign()的陷阱

不要高兴的太早! 虽然我们成功地创建了一个副本,一切似乎都正常工作,记得我们讨论了浅拷贝? 我们来看看这个例子:

let obj = {
a: 1,
b: {
c: 2,
},
}
let newObj = Object.assign({}, obj);
console.log(newObj); // { a: 1, b: { c: 2} }

obj.a = 10;
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 1, b: { c: 2} }

newObj.a = 20;
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 20, b: { c: 2} }

newObj.b.c = 30;
console.log(obj); // { a: 10, b: { c: 30} }
console.log(newObj); // { a: 20, b: { c: 30} }

// Note: newObj.b.c = 30; Read why..

obj.b.c = 30 ?

这就是 Object.assign() 的陷阱。Object.assign 只是浅拷贝。 newObj.b 和 obj.b 都引用同一个对象,没有单独拷贝,而是复制了对该对象的引用。任何对对象属性的更改都适用于使用该对象的所有引用。我们如何解决这个问题?继续阅读…我们会在下一节给出修复方案。 注意:原型链上的属性和不可枚举的属性不能复制。 看这里:

let someObj = {
a: 2,
}

let obj = Object.create(someObj, {
b: {
value: 2,
},
c: {
value: 3,
enumerable: true,
},
});

let objCopy = Object.assign({}, obj);
console.log(objCopy); // { c: 3 }

  • someObj 是在 obj 的原型链,所以它不会被复制。
  • property b 是不可枚举属性。
  • property c 具有 可枚举(enumerable) 属性描述符,所以它可以枚举。 这就是为什么它会被复制。

深度拷贝对象

深度拷贝将拷贝遇到的每个对象。副本和原始对象不会共享任何东西,所以它将是原件的副本。以下是使用 Object.assign() 遇到问题的修复方案。让我们探索一下。

使用 JSON.parse(JSON.stringify(object));

这可以修复了我们之前提出的问题。现在 newObj.b 有一个副本而不是一个引用!这是深度拷贝对象的一种方式。 这里有一个例子:

let obj = {
a: 1,
b: {
c: 2,
},
}

let 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 } } (New Object Intact!)

不可变性: ?

陷阱

不幸的是,此方法不能用于复制用户定义的对象方法。 见下文。

复制对象方法

方法是一个对象的属性,它是一个函数。在以上的示例中,我们还没有复制对象的方法。现在让我们尝试一下,使用我们学过的方法来创建副本。

let obj = {
name: ‘scotch.io’,
exec: function exec() {
return true;
},
}

let method1 = Object.assign({}, obj);
let method2 = JSON.parse(JSON.stringify(obj));

console.log(method1); //Object.assign({}, obj)
/* result
{
exec: function exec() {
return true;
},
name: “scotch.io”
}
*/

console.log(method2); // JSON.parse(JSON.stringify(obj))
/* result
{
name: “scotch.io”
}
*/

结果表明,Object.assign() 可以用于复制对象的方法,而使用 JSON.parse(JSON.stringify(obj)) 则不行。

复制循环引用对象

循环引用对象是具有引用自身属性的对象。让我们使用已学的复制对象的方法来复制一个循环引用对象的副本,看看它是否有效。

使用 JSON.parse(JSON.stringify(object))

让我们尝试使用 JSON.parse(JSON.stringify(object))

// circular object
let obj = {
a: ‘a’,
b: {
c: ‘c’,
d: ‘d’,
},
}

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);

结果是: 很明显,JSON.parse(JSON.stringify(object)) 不能用于复制循环引用对象。

使用 Object.assign()

让我们尝试使用 Object.assign()

// circular object
let obj = {
a: ‘a’,
b: {
c: ‘c’,
d: ‘d’,
},
}

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 newObj2 = Object.assign({}, obj);

console.log(newObj2);

结果是: Object.assign() 适用于浅拷贝循环引用对象,但不适用于深度拷贝。随意浏览浏览器控制台上的循环引用对象树。我相信你会发现很多有趣的工作在那里。

使用展开操作符(…)

ES6已经有了用于数组解构赋值的 rest 元素,和实现的数组字面展开的操作符。看一看这里的数组的展开操作符的实现:

const array = [
“a”,
“c”,
“d”, {
four: 4
},
];
const newArray = […array];
console.log(newArray);
// Result
// [“a”, “c”, “d”, { four: 4 }]

对象字面量的展开操作符目前是ECMAScript 的第 3 阶段提案。对象字面量的展开操作符能将源对象中的可枚举的属性复制到目标对象上。下面的例子展示了在提案被接受后复制一个对象是多么的容易。

let obj = {
one: 1,
two: 2,
}

let newObj = { …z };

// { one: 1, two: 2 }

注意:这将只对浅拷贝有效

结论

在 JavaScript 中复制对象可能是相当艰巨的,特别是如果您刚开始使用 JavaScript 并且不了解该语言的方式。希望本文帮助您了解并避免您可能遇到复制对象的陷阱。如果您有任何库或一段代码可以获得更好的结果,欢迎与社区分享。 文章来源:https://scotch.io

为了回馈我们的开发者社区,我们查看了数千个项目的数据库,发现了 JavaScript 的 10 大错误。我们将向你展示这些错误的原因,以及如何防止这些错误发生。如果你避免了这些 “陷阱” ,这将使你成为一个更好的开发人员。 由于数据是国王,我们收集,分析并排名前十的 JavaScript 错误。 Rollbar 会收集每个项目的所有错误,并总结每个项目发生的次数。 我们根据 指纹 对错误进行分组,来做到这一点。基本上,如果第二个错误只是第一个错误的重复,我们会把两个错误分到同一组。 这给用户一个很好的概括,而不是像在日志文件中看到的那些压迫性的一大堆垃圾描述。 我们专注于最有可能影响你和你的用户的错误。 为此,我们通过横跨不同公司的项目数来排列错误。 如果我们只查看每个错误发生的总次数,那么大流量的项目可能会淹没与大多数读者无关的错误的数据集。 以下是排名前 10 的 JavaScript 错误: 为了便于阅读,没有花大段的文字来描述每个错误。让我们深入到每一个错误,来确定什么可以导致它,以及如何避免它发生。

1.Uncaught TypeError: Cannot read property

如果你是一个 JavaScript 开发人员,你可能已经看到这个错误的次数比你敢承认的要多。当你读取一个属性或调用一个未定义的对象的方法时,这个错误会在 Chrome 中发生。你可以在 Chrome 开发者工具的控制台中轻松测试。 Screenshot of Uncaught TypeError: Cannot read property 发生这种情况的原因很多,但常见的一种情况是在渲染UI组件时不恰当地初始化了 state(状态)。 我们来看一个在真实应用程序中如何发生的例子。 我们将选择 React,但不正确初始化的原则也适用于Angular,Vue或任何其他框架。

class Quiz extends Component {
componentWillMount() {
axios.get(‘/thedata’).then(res => {
this.setState({ items: res.data });
});
}

render() {
    return (
        <ul>
    {this.state.items.map(item =>
      <li key={item.id}>{item.name}</li>
    )}
  </ul>
    );
}

}

这里有两件重要的事情要实现:

  1. 组件的状态(例如 this.state)从 undefined 开始。
  2. 当您异步获取数据时,组件在数据加载之前至少会渲染一次,而不管它是在构造函数 componentWillMount 还是 componentDidMount 中获取的。 当 Quiz 第一次渲染时,this.state.items 是 undefined 。 这又意味着 ItemList 将 items 定义为 undefined ,并且在控制台中出现错误 – “Uncaught TypeError: Cannot read property ‘map’ of undefined”。

这个问题很容易解决。最简单的方法:在构造函数中用合理的默认值来初始化 state。

class Quiz extends Component {
// Added this:
constructor(props) {
super(props);

    // Assign state itself, and a default value for items
    this.state = {
        items: \[\]
    };
}

componentWillMount() {
    axios.get('/thedata').then(res => {
        this.setState({ items: res.data });
    });
}

render() {
    return (
        <ul>
    {this.state.items.map(item =>
      <li key={item.id}>{item.name}</li>
    )}
  </ul>
    );
}

}

你的应用中的确切代码可能会有所不同,但是我们希望我们已经给了你足够的线索,来解决或避免在你的应用程序中出现这个问题。如果你还没有碰到,请继续阅读,因为我们将在下面覆盖更多相关错误的示例。

2.TypeError: ‘undefined’ is not an object (evaluating

这是在 Safari 中读取属性或调用未定义对象上的方法时发生的错误。你可以在 Safari Developer Console 中轻松测试。这与 Chrome 的上述错误基本相同,但 Safari 使用不同的错误消息。 Screenshot of TypeError: ‘undefined’ is not an object

3.TypeError: null is not an object (evaluating

这是在Safari中读取属性或调用 空对象(null) 上的方法时发生的错误。您可以在 Safari Developer Console中轻松测试。 Screenshot of TypeError: null is not an object 有趣的是,在 JavaScript 中,null 和 undefined 是不一样的,这就是为什么我们看到两个不同的错误信息。 undefined 通常是一个尚未分配的变量,而 null 表示该值为空。 要验证它们不相等,请尝试使用严格相等运算符 === : Screenshot of TypeError: null is not an object 在现实的例子中,这种错误可能发生的一种场景是:如果在加载元素之前尝试在 JavaScript 中使用 DOM 元素。这是因为 DOM API 对于空白的对象引用返回 null 。 任何执行和处理 DOM 元素的 JS 代码都应在 DOM 元素创建后执行。JS 代码按照 HTML 中的规定从上到下进行解析。所以,如果 DOM 元素之前有一个 script 标签, script 标签内的JS代码将在浏览器解析 HTML 页面时执行。如果在加载脚本之前尚未创建 DOM 元素,则会出现此错误。 在这个例子中,我们可以通过添加一个事件监听器来解决这个问题,这个监听器会在页面准备好的时候通知我们。一旦 addEventListener 被触发,init() 方法就可以使用 DOM 元素。

4.(unknown): Script error

当一个未捕获的 JavaScript 错误违反了跨域策略时,就会出现这类脚本错误。例如,如果你将 JavaScript 代码托管在 CDN 上,任何未被捕获的错误(这个会冒泡到 window.onerror 处理程序,而不是在 try-catch 捕获)将被报告为简单的 “脚本错误” ,而不会包含有用的信息。这是一种浏览器安全措施,旨在防止跨域传递数据,否则将不允许进行通信。 如果你要获取到真实的错误消息,请执行以下操作: 1.发送 Access-Control-Allow-Origin 头信息 将 Access-Control-Allow-Origin 头信息设置为 * ,表示可以从任何域正确访问资源。如有必要,您可以用你的域名替换 *,例如 Access-Control-Allow-Origin: www.example.com 。但是,处理多个域名会有些棘手,如果你使用 CDN ,由此出现的缓存问题可能会让你感觉不值得付出努力。 点击这里 看到更多。 下面是一些如何在不同环境中设置 Access-Control-Allow-Origin 头信息的例子。 Apache 在 JavaScript 文件所在的文件夹中,使用以下内容创建一个 .htaccess 文件:

Header add Access-Control-Allow-Origin “*“

Nginx 将 add_header 指令添加到提供 JavaScript 文件的位置块中:

location~ ^ /assets/ {
add_header Access - Control - Allow - Origin * ;
}

HAProxy 将以下内容添加到提供资源服务的后端,并提供 JavaScript 文件:

rspadd Access-Control-Allow-Origin:\ *

2.在 script 标签上设置 crossorigin=”anonymous” 属性 在你的 HTML 源代码中,对于你设置的 Access-Control-Allow-Origin 头信息的每个脚本,在 script 标签上设置 crossorigin="anonymous" 。在添加脚本标记上的 crossorigin 属性之前,请确保验证上述头信息是否正确发送。在 Firefox 中,如果存在 crossorigin 属性,但 Access-Control-Allow-Origin 头信息不存在,则脚本将不会执行。

5.TypeError: Object doesn’t support property

这是你在调用未定义方法时发生在IE中的错误。你可以在IE开发者工具的控制台进行测试。 Screenshot of TypeError: Object doesn’t support property 这相当于 Chrome 中的错误:”TypeError: ‘undefined’ is not a function” 。是的,对于相同的逻辑错误,不同的浏览器可能会有不同的错误消息。 在使用 JavaScript 命名空间的Web应用程序中,这中错误对于 IE 来说是一个常见问题。在这种情况下,这种问题 99.9% 是 IE 无法将当前名称空间内的方法绑定到 this 关键字。例如,如果你的 JS 命名空间 Rollbar 中有 isAwesome 方法。通常,如果你在 Rollbar 命名空间内,则可以使用以下语法调用 isAwesome 方法:

this.isAwesome();

Chrome,Firefox 和 Opera 会欣然地接受这个语法。 IE 则不会。 因此,使用 JS 命名空间时最安全的选择是始终以实际命名空间作为前缀。

Rollbar.isAwesome();

6.TypeError: ‘undefined’ is not a function

当你调用未定义的函数时,在 Chrome 中会发生这种错误。 你可以在 Chrome 开发者工具的控制台和 Mozilla Firefox 开发者工具的控制台中对此进行测试。 Screenshot of undefined is not a function 随着 JavaScript 编码技术和设计模式在这些年来越来越复杂,回调和闭包内的自引用作用域也相应增加,这是使用 this/that 混乱的一个相当常见的原因。 考虑这个示例代码片段:

function clearBoard() {
alert(“Cleared”);
}
document.addEventListener(“click”, function() {
this.clearBoard(); // what is “this” ?
});

如果你执行上面的代码然后点击页面,会导致以下错误: “Uncaught TypeError: this.clearBoard is not a function”。原因是正在执行的匿名函数在 document 上下文中, 而 clearBoard 定义在 window 中。 一个传统的,旧浏览器兼容的解决方案是简单地将你的 this 保存在一个变量,然后该变量可以被闭包继承。 例如:

var self = this; // save reference to ‘this’, while it’s still this!
document.addEventListener(“click”, function() {
self.clearBoard();
});

或者,在较新的浏览器中,可以使用 bind() 方法传递适当的引用:

document.addEventListener(“click”,this.clearBoard.bind(this));

7.Uncaught RangeError: Maximum call stack

这是 Chrome 在一些情况下会发生的错误。一个情况是当你调用一个不终止的递归函数时。你可以在Chrome开发者工具的控制台中进行测试。 Screenshot of Uncaught RangeError: Maximum call stack 如果你将一个值传递给超出范围的函数,也可能会发生这种情况。许多函数只接受其输入值的特定范围的数字。 例如,Number.toExponential(digits)Number.toFixed(digits) 接受0到20之间的数字, 和 Number.toPrecision(digits) 接受从1到21的数字。

var a = new Array(4294967295); //OK
var b = new Array(-1); //range error

var num = 2.555555;
document.writeln(num.toExponential(4)); //OK
document.writeln(num.toExponential(-2)); //range error!

num = 2.9999;
document.writeln(num.toFixed(2)); //OK
document.writeln(num.toFixed(25)); //range error!

num = 2.3456;
document.writeln(num.toPrecision(1)); //OK
document.writeln(num.toPrecision(22)); //range error!

8.TypeError: Cannot read property ‘length’

这是 Chrome 中发生的错误,因为读取未定义变量的长度属性。你可以在Chrome开发者工具的控制台中进行测试。 Screenshot of TypeError: Cannot read property ‘length’ 你通常会在数组中找到定义的长度,但是如果数组未初始化或变量名隐藏在另一个上下文中,则可能会遇到此错误。让我们用下面的例子来理解这个错误。

var testArray = [“Test”];
function testFunction(testArray) {
for (var i = 0; i < testArray.length; i++) {
console.log(testArray[i]);
}
}
testFunction();

当你用参数声明一个函数时,这些参数变成了本地参数。这意味着即使你有名称为 testArray 的变量,函数中具有相同名称的参数仍将被视为 本地参数。 你有两种方法可以解决这个问题: 1,删除函数声明语句中的参数(事实证明如果你想访问那些在函数之外声明的变量,你不需要将其作为你函数的参数传入):

var testArray = [“Test”];
/* Precondition: defined testArray outside of a function */
function testFunction( /* No params */ ) {
for (var i = 0; i < testArray.length; i++) {
console.log(testArray[i]);
}
}
testFunction();

2,调用函数时,将我们声明的数组传递给它:

var testArray = [“Test”];
function testFunction(testArray) {
for (var i = 0; i < testArray.length; i++) {
console.log(testArray[i]);
}
}
testFunction(testArray);

9.Uncaught TypeError: Cannot set property

当我们尝试访问一个未定义的变量时,它总是返回 undefined ,我们不能获取或设置任何 undefined 的属性。在这种情况下,应用程序将抛出 “Uncaught TypeError cannot set property of undefined.” 错误。 例如,在Chrome浏览器中: Screenshot of Uncaught TypeError: Cannot set property 如果 test 对象不存在,错误将会抛出 “Uncaught TypeError cannot set property of undefined.” 。

10.ReferenceError: event is not defined

当你尝试访问未定义的变量或超出当前作用域的变量时,会引发此错误。你可以在Chrome浏览器中轻松测试。 Screenshot of ReferenceError: event is not defined 如果你在使用事件处理时遇到这种错误,请确保你使用传入的事件对象作为参数。像IE这样的老浏览器提供了一个全局变量事件, Chrome 会自动将事件变量附加到处理程序。Firefox 不会自动添加它。像jQuery这样的库试图规范化这种行为。不过,最佳实践是使用传递到事件处理程序函数的方法。

document.addEventListener(“mousemove”, function(event) {
console.log(event);
})

总结

事实证明很多都是一些 null 或 undefined 错误。如果您使用严格的编译器选项,比如 Typescript 这样的好的静态类型检查系统可以帮助您避免它们。它可以警告你,如果一个类型是预期的,但尚未定义。 我们希望你学到了一些新的东西,并且可以避免将来出现这些错误,或者本指南帮助你解决了头痛的问题。尽管如此,即使有最佳做法,生产环境中还是会出现意想不到的错误。了解影响用户的错误非常重要,并有很好的工具来快速解决它们。 Rollbar 为你提供生产环境 JavaScript 错误的可视性,并为您提供更多上下文来快速解决它们。例如,它提供了额外的调试功能,例如 遥测功能,可以告诉你用户的浏览器发生了什么导致错误。这可以使在本地开发者工具的控制台之外发现问题。你可以在 Rollbar 的 JavaScript 应用程序的完整功能列表 中了解更多信息。 文章来源:https://rollbar.com/

不久以前,所有 HTML 页面的布局还都是通过 tables、floats 以及其他的 CSS 属性来完成的。面对复杂页面的布局,却没有很好的办法。 然而 Flexbox 的出现,便轻松的解决了复杂的 Web 布局。它是一种专注于创建稳定的响应式页面的布局模式,并可以轻松地正确对齐元素及其内容。如今已是大多数 Web 开发人员首选的 CSS 布局方式。 现在,又出现了一个构建 HTML 最佳布局体系的新竞争者。(霸主地位正在争夺中…)它就是强大的 CSS Grid 布局。直到本月月底,它也将在 Firefox 52 和 Chrome 57 上得到原生支持,相信不久也会得到其他浏览器兼容性的支持。

基本布局测试

要了解这两个体系构建布局的方式,我们将通过相同的 HTML 页面,利用不同的布局方式 (即 Flexbox 与 CSS Grid)为大家区分。同时,你也可以通过文章顶部附近的下载按钮,下载演示项目进行对比,或者通过在线演示来察看它们: 该页面的设计相对比较简单 – 它是由一个居中的容器组成,在其内部则包含了标头、主要内容部分、侧边栏和页脚。接下来,我们要完成同时保持 CSS 和 HTML 尽可能整洁的挑战事项:

  1. 在布局中将四个主要的部分进行定位。
  2. 将页面变为响应式页面;
  3. 对齐标头:导航朝左对齐,按钮向右对齐。

如你所见,为了便于比较,我们将所有事项从简处理。那么,让我们从第一个挑战事项开始吧!

挑战 1:定位页面部分

Flexbox 解决方案 我们将从 Flexbox 解决方案开始。我们将为容器添加 display: flex 来指定为 Flex 布局,并指定子元素的垂直方向。

.container {
display: flex;
flex-direction: column;
}

现在我们需要使主要内容部分和侧边栏彼此相邻。由于 Flex 容器通常是单向的,所以我们需要添加一个包装器元素。

然后,我们给包装器在反向添加 display: flex 和 flex-direction 属性。

.main-and-sidebar-wrapper {
display: flex;
flex-direction: row;
}

最后一步,我们将设置主要内容部分与侧边栏的大小。通过 Flex 实现后,主要内容部分会比侧边栏大三倍。

.main {
flex: 3;
margin-right: 60px;
}
.sidebar {
flex: 1;
}

如你所见,Flex 将其很好的实现了出来,但是仍需要相当多的 CSS 属性,并借助了额外的 HTML 元素。那么,让我们看看 CSS Grid 如何实现的。 CSS Grid 解决方案 针对本项目,有几种不同的 CSS Grid 解决方法,但是我们将使用网格模板区域语法来实现,因为它似乎最适合我们要完成的工作。 首先,我们将定义四个网格区域,所有的页面各一个:

header {
grid-area: header;
}
.main {
grid-area: main;
}
.sidebar {
grid-area: sidebar;
}
footer {
grid-area: footer;
}

然后,我们会设置网格并分配每个区域的位置。初次接触 Grid 布局的朋友,可能感觉以下的代码会有些复杂,但当你了解了网格体系,就很容易掌握了。

.container {
display: grid;

/\* Define the size and number of columns in our grid. 

The fr unit works similar to flex:
fr columns will share the free space in the row in proportion to their value.
We will have 2 columns - the first will be 3x the size of the second. */
grid-template-columns: 3fr 1fr;

/\* Assign the grid areas we did earlier to specific places on the grid. 

First row is all header.
Second row is shared between main and sidebar.
Last row is all footer. */
grid-template-areas:
“header header”
“main sidebar”
“footer footer”;

/\* The gutters between each grid cell will be 60 pixels. \*/
grid-gap: 60px;

}

就是这样! 我们现在将遵循上述结构进行布局,甚至不需要我们处理任何的 margins 或 paddings 。

挑战 2:将页面变为响应式页面

Flexbox 解决方案 这一步的执行与上一步密切相关。对于 Flexbox 解决方案,我们将更改包装器的 flex-direction 属性,并调整一些 margins。

@media (max-width: 600px) {
.main-and-sidebar-wrapper {
flex-direction: column;
}
.main {
margin-right: 0;
margin-bottom: 60px;
}
}

由于网页比较简单,所以我们在媒体查询上不需要太多的重写。但是,如果遇见更为复杂的布局,那么将会重新的定义相当多的内容。 CSS Grid 解决方案 由于我们已经定义了网格区域,所以我们只需要在媒体查询中重新排序它们。 我们可以使用相同的列设置。

@media (max-width: 600px) {
.container {
/* Realign the grid areas for a mobile layout. */
grid-template-areas:
“header header”
“main main”
“sidebar sidebar”
“footer footer”;
}
}

或者,我们可以从头开始重新定义整个布局。

@media (max-width: 600px) {
.container {
/* Redefine the grid into a single column layout. */
grid-template-columns: 1fr;
grid-template-areas:
“header”
“main”
“sidebar”
“footer”;
}
}

挑战 3:对齐标头组件

Flexbox 解决方案 我们的标头包含了导航和一个按钮的相关链接。我们希望导航朝左对齐,按钮向右对齐。而导航中的链接务必正确对齐,且彼此相邻。

我们曾在一篇较早的文章中使用 Flexbox 做了类似的布局:响应式标头最简单的制作方法。这个技术很简单:

header {
display: flex;
justify-content: space-between;
}

现在导航列表和按钮已正确对齐。下来我们将使 内的 items 进行水平移动。这里最简单的方法就是使用 display:inline-block 属性,但目前我们需要使用一个 Flexbox 解决方案:

header nav {
display: flex;
align-items: baseline;
}

仅两行代码就搞定了! 还不错吧。接下来让我们看看如何使用 CSS Grid 解决它。 CSS Grid 解决方案 为了拆分导航和按钮,我们要为标头定义 display: grid 属性,并设置一个 2 列的网格。同时,我们还需要两行额外的 CSS 代码,将它们定位在相应的边界上。

header {
display: grid;
grid-template-columns: 1fr 1fr;
}
header nav {
justify-self: start;
}
header button {
justify-self: end;
}

至于导航中的内链 – 这是我们使用 CSS grid 最好的布局展示: 虽然链接为内链形式,但它们不能正确的对齐。由于 CSS grid 不具备基线选项(不像 Flexbox 具备的 align-items 属性),所以我们只能再定义一个子网格。

header nav {
display: grid;
grid-template-columns: auto 1fr 1fr;
align-items: end;
}

CSS grid 在此步骤中,存在一些明显的布局上的缺陷。但你也不必过于惊讶。因为它的目标是对齐容器,而不是内部的内容。所以,用它来处理收尾工作,或许不是很好的选择哦。

结论

如果你已经浏览完整篇文章,那么结论不会让你感到意外。事实上,并不存在最好的布局方式。Flexbox 和 CSS grid 是两种不同的布局形式,我们应该根据具体的场景将它们搭配使用,而不是相互替代。 对于那些跳过文章只想看结论的朋友(不用担心,我们也这样做),这里是通过实例比较后的总结:

  • CSS Grid 适用于布局整体页面。它们使页面的布局变得非常容易,甚至可以处理一些不规则和非对称的设计。
  • Flexbox 非常适合对齐元素内的内容。你可以使用 Flexbox 来定位设计上一些较小的细节问题。
  • CSS Grid 适用于二维布局(行与列)。
  • Flexbox 适用于一维布局(行或列)。
  • 同时学习它们,并配合使用。

原文地址:https://tutorialzine.com

前言

有很多人搞不清匿名函数和闭包这两个概念,经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。匿名函数就是没有实际名字的函数。

闭包

概念 闭包,其实是一种语言特性,它是指的是程序设计语言中,允许将函数看作对象,然后能像在对象中的操作搬在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。 条件 闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到。 因此闭包的实现需要三个条件:

  1. 内部函数实用了外部函数的变量
  2. 外部函数已经退出
  3. 内部函数可以访问
    function a() {
        var x = 0;
        return function(y) {
            x = x + y;
            // return x;
            console.log(x);
        }
    }
    var b = a();
    b(1); //1
    b(1); //2
    上述代码在执行的时候,b得到的是闭包对象的引用,虽然a执行完毕后,但是a的活动对象由于闭包的存在并没有被销毁,在执行b(1)的时候,仍然访问到了x变量,并将其加1,若再执行b(1),则x2,因为闭包的引用b并没有消除。(后面会解释,闭包返回了函数,函数可以创建独立的作用域)

闭包,其实就是指程序语言中能让代码调用已运行的函数中所定义的局部变量。

但是你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。

function fn() {
    var max = 10;
    return function bar(x) {
        if (x > max) {
            console.log(x);
        }
    };
}
var f1 = fn();
f1(15);

如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。至于如何跨作用域取值,可以参考上一篇文章。

var max = 10,
    fn = function(x) {
        if (x > max) {
            console.log(x); //15
        }
    };
(function(f) {
    var max = 100;
    f(15);
})(fn);

如上代码中,fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10而不是100。 上一篇讲到自由变量跨作用域取值时,曾经强调过:要去创建这个函数的作用域取值,而不是“父作用域”。理解了这一点,以上两端代码中,自由变量如何取值应该比较简单. 另外,讲到闭包,除了结合着作用域之外,还需要结合着执行上下文栈来说一下。 在前面讲执行上下文栈时,我们提到当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。

有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。

可以拿本文的之前代码(只做注释修改)来分析一下。

//全局作用域
function fn() {
   var max = 10;
   // fn作用域
   return function bar(x) {
       if (x > max) {
           console.log(x);
       }
   }; //bar作用域
}
var f1 = fn();
f1(15);

全局作用域为:代码1-12行;fn作用域为:代码2-10行;bar作用域为:代码5-9行。 举例 第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。 第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。 第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:

因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。

因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。 ——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图: 第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。 执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。 这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。

总结:使用闭包会增加内容开销

第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。

闭包与变量

概念 闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊变量。 例子

function createFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result\[i\] = function() {
            return i;
        };
    }
    return result;
}
var funcs = createFunctions();
//每个函数都输出10
for (var i = 0; i < funcs.length; i++) {
    document.write(funcs\[i\]() + "<br />");
}

总结:每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值为10。

我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。

function createFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result\[i\] = function(x) {
            return function() {
                return x;
            };
        }(i);
    }
    return result;
}
var funcs = createFunctions();
//循环输出0-10
for (var i = 0; i < funcs.length; i++) {
    document.write(funcs\[i\]() + "<br />");
}

总结:没有直接把闭包赋值给数组,而是定义了一个匿名函数,并通过立即执行该匿名函数的结果赋值给数组,并带了for循环的参数i进去,让x能找到传入的参数值为0-10,这就解释了函数参数是按值传递的,所以会将变量i的当前值复制给参数x。而这个匿名函数内部又创建并返回了一个访问x的闭包。这样以来result数组中的每个函数都有自己x变量的一个副本,所以会符合我们的预期输出不同的值。

小例子 html结构代码:

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
</ul>

js结构代码:

// 闭包不符合预期
var clickBoxs = new Array();
clickBoxs = $("li");
for (var i = 0; i < 10; i++) {
    clickBoxs\[i\].onclick = function() {
        console.log(i);
    };
}

解决方法:

// 闭包1
var clickBoxs = new Array();
clickBoxs = $("li");
for (var i = 0; i < 10; i++) {
    clickBoxs\[i\].onclick = (function(x) {
        return function() {
            console.log(x);
            return x;
        }
    })(i);
}
// 闭包2
var clickBoxs = new Array();
clickBoxs = $("li");

function foo(i) {
    var onclick = function(e) {
        console.log(i);
    }
    return onclick;
}
for (var i = 0; i < 10; i++) {
    clickBoxs\[i\].onclick = foo(i);
}
// es6语法
var clickBoxs = new Array();
clickBoxs = $("li");
for (let i = 0; i < 10; i++) {
    clickBoxs\[i\].onclick = function() {
        console.log(i);
    };
}

函数按值传递 函数传参就两个类型,基本类型和引用类型,大家纠结的都是引用类型的传递。 引用类型作为参数传入函数,传的是个地址值,或者指针值,不是那个引用类型本身,它还好好的呆在堆内存呢。赋值给argument的同样是地址值或者指针。所以说是value值传递一点没错,传的是个地址值。通过两个例子看懂就行了。 例子1:

function setName(obj) {
    obj.name = 'aaa';
    var obj = new Object(); // 如果是按引用传递的,此处传参进来obj应该被重新引用新的内存单元
    obj.name = 'ccc';
    return obj;
}
var person = new Object();
person.name = 'bbb';
var newPerson = setName(person);
console.log(person.name + '  ' + newPerson.name); // aaa  ccc

从结果看,并没有显示两个’ccc’。这里是函数内部重写了obj,重写的obj是一个局部对象。当函数执行完后,立即被销毁。

引用值:对象变量它里面的值是这个对象在堆内存中的内存地址。因此如果按引用传递,它传递的值也就是这个内存地址。那么var obj = new Object();会重新给obj分配一个地址,比如是0x321了,那么它就不在指向有name = ‘aaa’;属性的内存单元了。相当于把实参obj和形参obj的地址都改了,那么最终就是输出两个ccc了。

例子2

var a = {
    num: '1'
};
var b = {
    num: '2'
};

function change(obj) {
    obj.num = '3';
    obj = b;
    return obj.num;
}
var result = change(a);
console.log(result + '  ' + a.num); // 2  3
  • 首先把a的值传到change函数内,obj.num = ‘3’;后a.name被修改为3;
  • a的地址被换成b的地址;
  • 返回此时的a中a.num。

闭包中使用this对象

概念 this对象是在运行时基于函数的执行环境绑定的:全局函数中,this等于window;当函数被作用某个对象的方法调用时,this等于那个对象。 但在匿名函数中,由于匿名函数的执行环境具有全局性,因此this对象通常指向window(在通过call或apply函数改变函数执行环境的情况下,会指向其他对象)。

var name = "The Window";

var object = {
    name: "My Object",

    getNameFunc: function() {
        return function() {
            return this.name;
        };
    }
};

console.log(object.getNameFunc()()); //"The Window"

通过修改把作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。如下代码:

var name = "The Window";

var object = {
    name: "My Object",

    getNameFunc: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};

console.log();
(object.getNameFunc()()); //"MyObject"

变量声明提前

var scope = "global";

function scopeTest() {
    console.log(scope);
    var scope = "local";
}
scopeTest(); //undefined

此处的输出是`undefined`,并没有报错,这是因为在前面我们提到的函数内的声明在函数体内始终可见,上面的函数等效于:

var scope = "global";

function scopeTest() {
    var scope;
    console.log(scope);
    scope = "local";
}
scopeTest(); //undefined

注意,如果忘记var,那么变量就被声明为全局变量了。结果就是global

没有块级作用域

和其他我们常用的语言不同,在Javascript中没有块级作用域:

function scopeTest() {
    var scope = {};
    if (scope instanceof Object) {
        var j = 1;
        for (var i = 0; i < 10; i++) {
            console.log(i); //输出0-9
        }
        console.log(i); //输出10 
    }
    console.log(j); //输出1 
}
scopeTest();

在javascript中变量的作用范围是函数级的,即在函数中所有的变量在整个函数中都有定义,这也带来了一些我们稍不注意就会碰到的“潜规则”:

var scope = “hello”;

function scopeTest() {
console.log(scope); //①
var scope = “no”;
console.log(scope); //②
}

在①处输出的值竟然是`undefined`,简直丧心病狂啊,我们已经定义了全局变量的值啊,这地方不应该为hello吗?其实,上面的代码等效于:

var scope = “hello”;

function scopeTest() {
var scope;
console.log(scope); //①
scope = “no”;
console.log(scope); //②
}

```
声明提前、全局变量优先级低于局部变量,根据这两条规则就不难理解为什么输出undefined了。

使用 JavaScript 时,我们经常需要处理很多条件语句,这里分享5个小技巧,可以让你编写更好/更清晰的条件语句。

1.使用 Array.includes 来处理多个条件

我们来看看下面的例子:

// condition
function test(fruit) {
if (fruit == ‘apple’ fruit == ‘strawberry’) {
console.log(‘red’);
}
}

乍一看,上面的例子看起来似乎没什么问题。 但是,如果我们还有更多的红色水果呢?比如樱桃(cherry)和蔓越莓(cranberries)。 我们是否要用更多的 操作符来扩展该语句呢? 我们可以使用 Array.includes 重写上面的条件语句。

function test(fruit) {
// 条件提取到数组中
const redFruits = [‘apple’, ‘strawberry’, ‘cherry’, ‘cranberries’];
if (redFruits.includes(fruit)) {
console.log(‘red’);
}
}

我们将红色水果(条件)提取到一个数组中。这样做,可以让代码看起来更整洁。

2.减少嵌套,提前使用 return 语句

让我们扩展前面的示例,再包含另外两个条件:

  • 如果没有提供水果,抛出错误
  • 接受水果 quantity(数量)参数,如果超过 10,则并打印相关信息。

function test(fruit, quantity) {
const redFruits = [‘apple’, ‘strawberry’, ‘cherry’, ‘cranberries’];
// 条件 1:fruit 必须有值
if (fruit) {
// 条件 2:必须为红色
if (redFruits.includes(fruit)) {
console.log(‘red’);

        // 条件 3:数量必须大于 10
        if (quantity > 10) {
            console.log('big quantity');
        }
    }
} else {
    throw new Error('No fruit!');
}

}

// 测试结果
test(null); // 抛出错误:No fruits
test(‘apple’); // 打印:red
test(‘apple’, 20); // 打印:red,big quantity

看看上面的代码,我们有: – 1 个 if / else 语句过滤掉无效条件 – 3 层 if 语句嵌套(分别是条件1,2和3) 我个人遵循的一般规则是 在发现无效条件时提前 return

/* 在发现无效条件时提前 return */
function test(fruit, quantity) {
const redFruits = [‘apple’, ‘strawberry’, ‘cherry’, ‘cranberries’];

// 条件 1:提前抛出错误
if (!fruit) throw new Error('No fruit!');

// 条件2:必须为红色
if (redFruits.includes(fruit)) {
    console.log('red');

    // 条件 3:数量必须大于 10
    if (quantity > 10) {
        console.log('big quantity');
    }
}

}

这样做,我们可以减少一个嵌套层级。 这种编码风格很好,特别是当你的 if 语句很长时(想象一下,你需要滚动到最底部才知道那里有一个 else 语句,这样代码的可读性就变得很差了)。 如果通过反转条件并提前 return ,我们可以进一步减少嵌套。 请查看下面的条件 2 ,看看我们是如何做到的:

/* 在发现无效条件时提前 return */
function test(fruit, quantity) {
const redFruits = [‘apple’, ‘strawberry’, ‘cherry’, ‘cranberries’];

if (!fruit) throw new Error('No fruit!'); // 条件 1:提前抛出错误
if (!redFruits.includes(fruit)) return; // 条件 2:当 fruit 不是红色的时候,提前 return

console.log('red');

// 条件 3:必须是大量存在
if (quantity > 10) {
    console.log('big quantity');
}

}

通过反转条件2的条件,我们的代码现在没有嵌套语句了。 当我们有很长的逻辑代码时,这种技巧非常有用,我们希望在条件不满足时停止下一步的处理。 然而,这并不是严格的规定。问问自己,这个版本(没有嵌套)是否要比前一个版本(条件 2 有嵌套)的更好、可具可读性? 对我来说,我会选择前一个版本(条件 2 有嵌套)。 这是因为:

  • 代码简短直接,嵌套 if 更清晰
  • 反转条件可能会引发更多的思考过程(增加认知负担)

因此,始终追求更少的嵌套,提前 return,但是不要过度。但不要过度。如果您感兴趣,这里有一篇文章和 StackOverflow 的讨论, 进一步讨论这个话题:

3.使用函数的默认参数 和 解构

我想下面的代码可能看起来很熟悉,我们在使用 JavaScript 时总是需要检查 null / undefined 值并分配默认值:

function test(fruit, quantity) {
if (!fruit) return;
const q = quantity 1; // 如果没有提供 quantity 参数,则默认为 1

console.log(\`We have ${q} ${fruit}!\`);

}

// 测试结果
test(‘banana’); // We have 1 banana!
test(‘apple’, 2); // We have 2 apple!

实际上,我们可以通过分配默认函数参数来消除变量 q

function test(fruit, quantity = 1) { // i如果没有提供 quantity 参数,则默认为 1
if (!fruit) return;
console.log(`We have ${quantity} ${fruit}!`);
}

// 测试结果
test(‘banana’); // We have 1 banana!
test(‘apple’, 2); // We have 2 apple!

更简单直观不是吗? 请注意,每个函数参数都有自己的默认值。 例如,我们也可以为 fruit 分配一个默认值:function test(fruit = 'unknown', quantity = 1)。 如果我们的 fruit 是一个 Object 对象怎么办? 我们可以指定默认参数吗?

function test(fruit) {
// 如果有值,则打印 fruit.name
if (fruit && fruit.name) {
console.log(fruit.name);
} else {
console.log(‘unknown’);
}
}

//测试结果
test(undefined); // unknown
test({}); // unknown
test({ name: ‘apple’, color: ‘red’ }); // apple

看看上面的例子,我们想要的是如果 fruit.name 可用则打印水果名称,否则将打印 unknown 。我们可以使用默认函数参数和解构(destructing) 来避免 fruit && fruit.name 这样的检查。

// 解构 —— 只获得 name 属性
// 参数默认分配空对象 {}
function test({ name } = {}) {
console.log(name ‘unknown’);
}

//测试结果
test(undefined); // unknown
test({}); // unknown
test({ name: ‘apple’, color: ‘red’ }); // apple

由于我们只需要来自 fruitname 属性,我们可以使用 {name} 来解构参数,然后我们可以在代码中使用 name 作为变量来取代fruit.name。 我们还将空对象 {} 指定为默认值。 如果我们不这样做,你将在执行行测试时遇到test(undefined) – Cannot destructure property name of 'undefined' or 'null'.(无法解析’undefined’或’null’的属性名称)。 因为 undefined中 没有 name 属性。 如果您不介意使用第三方库,有几种方法可以减少空检查:

  • 使用 Lodash get 函数
  • 使用 Facebook 开源的 idx 库(需搭配 Babeljs)

以下是使用Lodash的示例:

// 引入 lodash 库,我们将获得 _.get()
function test(fruit) {
console.log(_.get(fruit, ‘name’, ‘unknown’); // 获取 name 属性,如果没有分配,则设为默认值 unknown
}

//测试结果
test(undefined); // unknown
test({ }); // unknown
test({ name: ‘apple’, color: ‘red’ }); // apple

您可以在这里 运行演示代码 。此外,如果你喜欢函数式编程(FP),您可以选择使用Lodash fp ,Lodash的函数式能版本(方法名更改为 get 或 getOr)。

4.选择 Map / Object 字面量,而不是Switch语句

让我们看看下面的例子,我们想根据颜色打印水果:

function test(color) {
// 使用 switch case 语句,根据颜色找出对应的水果
switch (color) {
case ‘red’:
return [‘apple’, ‘strawberry’];
case ‘yellow’:
return [‘banana’, ‘pineapple’];
case ‘purple’:
return [‘grape’, ‘plum’];
default:
return [];
}
}

//测试结果
test(null); // []
test(‘yellow’); // [‘banana’, ‘pineapple’]

上面的代码似乎没有错,但我觉得它很冗长。使用具有更清晰语法的 object 字面量可以实现相同的结果:

// 使用对象字面量,根据颜色找出对应的水果
const fruitColor = {
red: [‘apple’, ‘strawberry’],
yellow: [‘banana’, ‘pineapple’],
purple: [‘grape’, ‘plum’]
};

function test(color) {
return fruitColor[color] [];
}

或者,您可以使用 Map 来实现相同的结果:

// 使用 Map ,根据颜色找出对应的水果
const fruitColor = new Map()
.set(‘red’, [‘apple’, ‘strawberry’])
.set(‘yellow’, [‘banana’, ‘pineapple’])
.set(‘purple’, [‘grape’, ‘plum’]);

function test(color) {
return fruitColor.get(color) [];
}

Map 是 ES2015(ES6) 引入的新的对象类型,允许您存储键值对。 我们是不是应该禁止使用 switch 语句呢? 不要局限于此。 就个人而言,我尽可能使用对象字面量,但我不会设置硬规则来阻止使用 switch ,是否使用应该根据你的场景而决定。 Todd Motto 有一篇文章深入地研究了 switch语句与对象字面量,你可以在 这里 阅读。 重构语法 对于上面的示例,我们实际上可以使用 Array.filter 来重构我们的代码,以实现相同的结果。

const fruits = [
{ name: ‘apple’, color: ‘red’ },
{ name: ‘strawberry’, color: ‘red’ },
{ name: ‘banana’, color: ‘yellow’ },
{ name: ‘pineapple’, color: ‘yellow’ },
{ name: ‘grape’, color: ‘purple’ },
{ name: ‘plum’, color: ‘purple’ }
];

function test(color) {
// 使用 Array filter ,根据颜色找出对应的水果

return fruits.filter(f => f.color == color);

}

总有不止一种方法可以达到相同的效果。对于这个例子我们展示了 4 种实现方法。编码很有趣!

5. 使用 Array.every 和 Array.some 来处理全部/部分满足条件

最后一个小技巧更多地是利用新的(但不是那么新的)Javascript Array函数来减少代码行。查看下面的代码,我们想检查所有水果是否都是红色的:

const fruits = [
{ name: ‘apple’, color: ‘red’ },
{ name: ‘banana’, color: ‘yellow’ },
{ name: ‘grape’, color: ‘purple’ }
];

function test() {
let isAllRed = true;

// 条件:所有的水果都必须是红色
for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed = (f.color == 'red');
}

console.log(isAllRed); // false

}

代码太长了!我们可以使用 Array.every 减少行数:

const fruits = [
{ name: ‘apple’, color: ‘red’ },
{ name: ‘banana’, color: ‘yellow’ },
{ name: ‘grape’, color: ‘purple’ }
];

function test() {
// 条件:简短方式,所有的水果都必须是红色
const isAllRed = fruits.every(f => f.color == ‘red’);

console.log(isAllRed); // false

}

干净多了对吧?类似的,如果我们想要检查是否有至少一个水果是红色的,我们可以使用 Array.some 仅用一行代码就实现出来。

const fruits = [
{ name: ‘apple’, color: ‘red’ },
{ name: ‘banana’, color: ‘yellow’ },
{ name: ‘grape’, color: ‘purple’ }
];

function test() {
// 条件:是否存在红色的水果
const isAnyRed = fruits.some(f => f.color == ‘red’);

console.log(isAnyRed); // true

}

  原文地址:https://scotch.io