zzard

zzard 查看完整档案

昆明编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

认真不足,天真有余。

个人动态

zzard 赞了文章 · 2020-12-28

现代JavaScript:ES6+ 中的 Imports,Exports,Let,Const 和 Promise

在过去几年里,JavaScript有很多的更新。如果你想提升写代码的能力,这些更新将会对你有非常大的帮助。

对于程序员来说,了解这门语言的最新发展是非常重要的。它能使你跟上最新趋势,提高代码质量,在工作中出类拔萃,从而进一步提升你的薪资待遇。

特别地,如果你想学习像React、 Angular或Vue这样的框架,你必须掌握这些最新的特性。

最近,JavaScript增加了许多有用的功能,比如Nullish coalescing operator, optional chaining, Promises, async/await, ES6 destructuring,等等。

那么现在,我们将探讨每个JavaScript开发者都应该知道的概念。

JavaScript 中的 Let 和 const

在ES6之前,JavaScript使用var关键字来声明变量,var只有全局作用域和函数作用域,所谓全局作用域就是在代码的任何位置都能访问var声明的变量,而函数作用域在变量声明的当前函数内部访问变量。此时是没有块级作用域的。

随着let和const这两个关键字的添加,JS增加了块级作用域的概念。

如何在 JavaScript中使用let

当我们在用let声明变量时,用于声明一次之后就不能再以相同的名称重新声明它。

// ES5 Code

var value =  10;

console.log(value);  // 10

var value =  "hello";

console.log(value);  // hello

var value =  30;

console.log(value);  // 30

如上所示,我们多次使用var关键字重新声明了变量值。

在ES6之前,我们可以使用var重新声明之前已经声明过的变量,这就会导致了一个问题:如果我们在不知情的情况下,在其他地方重新声明了该变量,很有可能会覆盖原先变量的值,造成一些难以调试的问题。

所以,Let解决很好地解决此问题。当你使用let重新声明变量值时,将会报错。

// ES6 Code
let value = 10;
console.log(value); // 10

let value = "hello"; // Uncaught SyntaxError: Identifier 'value' has already been declared

但是,以下代码是合法的:

// ES6 Code
let value = 10;
console.log(value); // 10

value = "hello";
console.log(value); // hello

我们发现上面的代码看起来没什么问题,是因为我们重新给value变量赋了一个新值,但是并没有重新声明。

我们来看下下面的代码:

// ES5 Code
var isValid = true;
if(isValid) {
  var number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10

如上所示,在使用var声明变量时,可以在if块之外访问该变量。

而使用let声明的number变量只能在if块内访问,如果在if块外访问将会报错。

我们来看下接下来的代码


// ES6 Code
let isValid = true;
if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // Uncaught ReferenceError: number is not defined

如上述代码所示,使用let分别在if块内、if块外声明了number变量。在if块外,number无法被访问,因此会出现引用错误。

但是,如果变量number在if块外已经声明,将会出现下面的结果。


// ES6 Code
let isValid = true;
let number = 20;

if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}

console.log('outside:', number); // outside: 20

现在在单独的范围内有两个number变量。在if块外,number的值为20。

 // ES5 Code
for(var i = 0; i < 10; i++){
 console.log(i);
}
console.log('outside:', i); // 10


当使用var关键字时,i在 for循环之外也可以访问到。


// ES6 Code
for(let i = 0; i < 10; i++){
 console.log(i);
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined

而使用let关键字时,在for循环外部是不可访问的。

因此,正如上述示例代码所示,let声明的变量只能在块内部可用,而在块外部不可访问。

我们可以使用一对大括号创建一个块,如下:

let i = 10;
{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // outside: 10

前面有提到,let在同一个块中不能重新声明变量,不过可以在另一个块中重新声明。如上代码所示,我们在块内重新声明了i,并赋值20,该变量仅可在该块中使用。

在块外,当我们打印变量时,我们得到的是10而不是之前分配的值,这是因为块外,内部变变量i是不存在的。

如果在块外未声明变量,那么将会报错:


{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined

如何在 JavaScript使用const

const关键字在块级作用域中的工作方式与let关键字完全相同。因此,我们来看下他们的区别。

const声明的变量为常量,其值是不能改变的。而let声明的变量,可以为其赋一个新值,如下所示:

let number = 10;
number = 20;
console.log(number); // 20

但是以下情况,我们不能这样使用const。


const number = 10;
number = 20; // Uncaught TypeError: Assignment to constant variable.

我们甚至不能使用const像下面一样重新声明。

const number = 20;
console.log(number); // 20

const number = 10; // Uncaught SyntaxError: Identifier 'number' has already been declared

现在,看下面的代码:

const arr = [1, 2, 3, 4];
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]

我们说过const声明的常量,它的值永远不会改变——但是我们改变了上面的常量数组并没有报错。这是为什么呢?

注意:数组是引用类型,而不是JavaScript的基本类型

实际存储在arr中的不是数组,而是数组存储的内存位置的引用(地址)。执行arr.push(5),并没有改变arr指向的引用,而是改变了存储在那个引用上的值。

对象也是如此:

const obj = {
 name: 'David',
 age: 30
};

obj.age = 40;

console.log(obj); // { name: 'David', age: 40 }

这里,我们也没有改变obj指向的引用,而是改变了存储在引用的值。

因此,上述的代码将会起作用,但下面的代码是无效的。

const obj = { name: 'David', age: 30 };
const obj1 = { name: 'Mike', age: 40 };
obj = obj1; // Uncaught TypeError: Assignment to constant variable.

这样写会抛出异常,因为我们试图更改const变量指向的引用。

因此,在使用const时要记住一点:使用const声明常量时,不能重新声明,也不能重新赋值。如果声明的常量是引用类型,我们可以更改存储在引用的值。

同理,下面的代码也是无效的。

const arr = [1, 2, 3, 4];
arr = [10, 20, 30]; // Uncaught TypeError: Assignment to constant variable.

总结:

  • 关键字let和const在JavaScript中添加块级作用域。
  • 当我们将一个变量声明为let时,我们不能在同一作用域(函数或块级作用域)中重新定义或重新声明另一个具有相同名称的let变量,但是我们可以重新赋值。
  • 当我们将一个变量声明为const时,我们不能在同一作用域(函数或块级作用域)中重新定义或重新声明具有相同名称的另一个const变量。但是,如果变量是引用类型(如数组或对象),我们可以更改存储在该变量中的值。

好了,我们继续下一个话题: promises。

JavaScript中的promises

对于很多新开发者来说,promises是JavaScript中较难理解的部分。ES6中原生提供了Promise对象,那么Promise究竟是什么呢?

Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。

如何创造一个 promise

使用promise构造函数创建一个promise,如下所示:

const promise = new Promise(function(resolve, reject) {
 
});

Promise的构造函数接收一个函数作为参数,并且在内部接收两个参数:resolve,reject。 resolve和reject参数实际上是我们可以调用的函数,具体取决于异步操作的结果。

Promise 有三种状态:

  • pending: 初始状态,不是成功或失败状态。
  • fulfilled:表示操作成功完成。
  • rejected: 表示操作失败。

当我们创建Promise时,它处于等待的状态。当我们调用resolve函数时,它将进入已完成状态。如果调用reject,他将进入被拒绝状态。

在下面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,调用resolve方法。

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});

我们需要使用以下方法注册一个回调.then获得1promise执行成功的结果,如下所示:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});

promise.then(function(result) {
 console.log(result); // 9
});

then接收一个参数,是函数,并且会拿到我们在promise中调用resolve时传的的参数。

如果操作不成功,则调用reject函数:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
});

如果sum不是一个数字,那么我们调用带有错误信息的reject函数,否则我们调用resolve函数。

执行上述代码,输出如下:

image.png

调用reject函数会抛出一个错误,但是我们没有添加用于捕获错误的代码。

需要调用catch方法指定的回调函数来捕获并处理这个错误。

promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

输出如下:

image.png

所以建议大家在使用promise时加上catch方法,以此来避免程序因错误而停止运行。

链式操作

我们可以向单个promise添加多个then方法,如下所示:

promise.then(function(result) {
 console.log('first .then handler');
 return result;
}).then(function(result) {
 console.log('second .then handler');
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

当添加多个then方法时,前一个then方法的返回值将自动传递给下一个then方法。

image.png

如上图所示,我们在第一个then方法中输出字符串,并将接收的参数result(sum)返回给下一个result。

在下一个then方法中,输出字符串,并输出上一个then方法传递给它的result。

如何在 JavaScript中延迟promise的执行

很多时候,我们不希望立即创建promise,而是希望在某个操作完成后再创建。

我们可以将promise封装在一个函数中,然后从函数中返回promise,如下所示:

function createPromise() {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = 4 + 5;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}

这样,我们就可以通过函数将参数传递给promise,达到动态的目的。

function createPromise(a, b) {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = a + b;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}

createPromise(1,8)
 .then(function(output) {
  console.log(output); // 9
});

// OR

createPromise(10,24)
 .then(function(output) {
  console.log(output); // 34
});

image.png

此外,我们只能向resolve或reject函数传递一个值。如果你想传递多个值到resolve函数,可以将它作为一个对象传递,如下:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve({
   a: 4,
   b: 5,
   sum
  });
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

image.png

如何在 JavaScript中使用箭头函数

上述示例代码中,我们使用常规的ES5语法创建了promise。但是,通常使用箭头函数代替ES5语法,如下:

const promise = new Promise((resolve, reject) => {
 setTimeout(() => {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then((result) => {
 console.log(result);
});

你可以根据自己需要使用ES5或ES6语法。

ES6 Import 和Export 语法

在ES6之前,我们在一个HTML文件中可以使用多个script标签来引用不同的JavaScript文件,如下所示:

<script type="text/javascript" data-original="home.js"></script>
<script type="text/javascript" data-original="profile.js"></script>
<script type="text/javascript" data-original="user.js"></script>

但是如果我们在不同的JavaScript文件中有一个同名的变量,将会出现命名冲突,你实际得到的可能并不是你期望的值。

ES6增加了模块的概念来解决这个问题。

在ES6中,我们编写的每一个JavaScript文件都被称为模块。我们在每个文件中声明的变量和函数不能用于其他文件,除非我们将它们从该文件中导出并、在另一个文件中得到引用。

因此,在文件中定义的函数和变量是每个文件私有的,在导出它们之前,不能在文件外部访问它们。

export有两种类型:

  • 命名导出:在一个文件中可以有多个命名导出
  • 默认导出:单个文件中只能有一个默认导出

JavaScript中的命名导出

如下所示,将单个变量命名导出:

export const temp = "This is some dummy text";

如果想导出多个变量,可以使用大括号指定要输出的一组变量。

const temp1 = "This is some dummy text1";
const temp2 = "This is some dummy text2";
export { temp1, temp2 };

需要注意的是,导出语法不是对象语法。因此,在ES6中,不能使用键值对的形式导出。</pre>

          // This is invalid syntax of export in ES6
           export { key1: value1, key2: value2 }

import命令接受一对大括号,里面指定要从其他模块导入的变量名。

           import { temp1, temp2 } from './filename';

注意,不需要在文件名中添加.js扩展名,因为默认情况下会考虑该拓展名。


 // import from functions.js file from current directory

import  { temp1, temp2 }  from  './functions';

 // import from functions.js file from parent of current directory

import  { temp1 }  from  '../functions';

提示一点,导入的变量名必须与被导入模块对外接口的名称相同。

因此,导出应使用:

// constants.js

export  const  PI  =  3.14159;

那么在导入的时候,必须使用与导出时相同的名称:

import  {  PI  }  from  './constants';

// This will throw an error

import  { PiValue }  from  './constants';

如果想为输入的变量重新命名,可以使用as关键字,语法如下:

import  {  PI  as PIValue }  from  './constants';

我们以为PI重命名为PIValue,因此不能再使用PI变量名。

导出时也可使用下面的重命名语法:

// constants.js

const  PI  =  3.14159;

export  {  PI  as PIValue };

然后在导入是,必须使用PIValue。

import  { PIValue }  from  './constants';

命名导出某些内容之前必须先声明它。

export  'hello';  // this will result in error

export  const greeting =  'hello';  // this will work

export  { name:  'David'  };  // This will result in error

export  const object =  { name:  'David'  };  // This will work

我们来看下面的validations.js 文件:

// utils/validations.js

const  isValidEmail  =  function(email)  {

  if  (/^[^@ ]+@[^@ ]+\.[^@ \.]{2,}$/.test(email))  {

  return  "email is valid";

  }  else  {

  return  "email is invalid";

  }

};

const  isValidPhone  =  function(phone)  {

  if  (/^[\\(]\d{3}[\\)]\s\d{3}-\d{4}$/.test(phone))  {

  return  "phone number is valid";

  }  else  {

  return  "phone number is invalid";

  }

};

function  isEmpty(value)  {

  if  (/^\s*$/.test(value))  {

  return  "string is empty or contains only spaces";

  }  else  {

  return  "string is not empty and does not contain spaces";

  }

}

export  { isValidEmail, isValidPhone, isEmpty };

在index.js中,我们可以使用如下函数:

// index.js

import  { isEmpty, isValidEmail }  from  "./utils/validations";

console.log("isEmpty:",  isEmpty("abcd"));  // isEmpty: string is not empty and does not contain spaces

console.log("isValidEmail:",  isValidEmail("abc@11gmail.com"));  // isValidEmail: email is valid

console.log("isValidEmail:",  isValidEmail("ab@c@11gmail.com"));  // isValidEmail: email is invalid

JavaScript的默认导出

如上所述,单个文件中最多只能有一个默认导出。但是,你可以在一个文件中使用多个命名导出和一个默认导出。

要声明一个默认导出,我们需要使用以下语法:

//constants.js

const name =  'David';

export  default name;

在导入时就不需要再使用花括号了。

import name from  './constants';

如下,我们有多个命名导出和一个默认导出:

// constants.js

export  const  PI  =  3.14159;

export  const  AGE  =  30;

const  NAME  =  "David";

export  default  NAME;

此时我们使用import导入时,只需要在大括号之前指定默认导出的变量名。

// NAME is default export and PI and AGE are named exports here

import  NAME,  {  PI,  AGE  }  from  './constants';

使用 export default 导出的内容,在导入的时候,import后面的名称可以是任意的。

// constants.js

const  AGE  =  30;

export  default  AGE;

import myAge from ‘./constants’;

console.log(myAge);  // 30

另外, export default的变量名称从Age到myAge之所以可行,是因为只能存在一个export default。因此你可以随意命名。还需注意的是,关键字不能在声明变量之前。

// constants.js

export  default  const  AGE  =  30;  // This is an error and will not work

因此,我们需要在单独的一行使用关键字。

// constants.js

const  AGE  =  30;

export  default  AGE;

不过以下形式是允许的:

//constants.js

export  default  {

 name:  "Billy",

 age:  40

};

并且需要在另一个文件中使用它

import user from  './constants';

console.log(user.name);  // Billy

console.log(user.age);  // 40

还有,可以使用以下语法来导入constants.js文件中导出的所有变量:

// test.js

import  *  as constants from  './constants';

下面,我们将导入所有我们constants.js存储在constants变量中的命名和export default。因此,constants现在将成为对象。

// constants.js

export  const  USERNAME  =  "David";

export  default  {

 name:  "Billy",

 age:  40

};

在另一个文件中,我们按一下方式使用。

// test.js

import  *  as constants from  './constants';

console.log(constants.USERNAME);  // David

console.log(constants.default);  // { name: "Billy", age: 40 }

console.log(constants.default.age);  // 40

也可以使用以下方式组合使用命名导出和默认导出:

// constants.js

const  PI  =  3.14159;  const  AGE  =  30;

const  USERNAME  =  "David";

const  USER  =  {

 name:  "Billy",

 age:  40

};

export  {  PI,  AGE,  USERNAME,  USER  as  default  };

import  USER,  {  PI,  AGE,  USERNAME  }  from  "./constants";

总而言之:

ES6中,一个模块就是一个独立的文件,该文件内部的所有变量,外部都无法获取。如果想从外部读取模块内的某个变量,必须使用export关键字导出该变量,使用import关键字导入该变量。

JavaScript中的默认参数

ES6增加了一个非常有用的特性,即在定义函数时提供默认参数。

假设我们有一个应用程序,一旦用户登录系统,我们将向他们显示一条欢迎消息,如下所示:

function  showMessage(firstName)  {

  return  "Welcome back, "  + firstName;

}

console.log(showMessage('John'));  // Welcome back, John

但是,如果数据库中没有用户名,那该怎么办呢?所以,我们首先需要检查是否提供了firstName,然后再显示相应的信息。

在ES6之前,我们必须写这样的代码:

function  showMessage(firstName)  {

  if(firstName)  {

  return  "Welcome back, "  + firstName;

  }  else  {

  return  "Welcome back, Guest";

  }

}

console.log(showMessage('John'));  // Welcome back, John

console.log(showMessage());  // Welcome back, Guest

但现在使用ES6提供的默认参数,我们可以这样写:

function  showMessage(firstName =  'Guest')  {

  return  "Welcome back, "  + firstName;

}

console.log(showMessage('John'));  // Welcome back, John

console.log(showMessage());  // Welcome back, Guest

函数的默认参数可以为任意值。

function  display(a =  10, b =  20, c = b)  {

 console.log(a, b, c);

}

display();  // 10 20 20

display(40);  // 40 20 20

display(1,  70);  // 1 70 70

display(1,  30,  70);  // 1 30 70

在上面的代码中,我们没有提供函数的所有参数,实际代码等同于:

display();  // 等同于display(undefined, undefined, undefined)

display(40);  等同于display(40, undefined, undefined)

display(1,  70);  等同于display(1, 70, undefined)

因此,如果传递的参数是undefined,则对应的参数将使用默认值。

我们还可以将对象或计算值指定为默认值,如下:

const defaultUser =  {

 name:  'Jane',

 location:  'NY',

 job:  'Software Developer'

};

const  display  =  (user = defaultUser, age =  60  /  2  )  =>  {

 console.log(user, age);

};

display();

/* output

{

 name: 'Jane',

 location: 'NY',

 job: 'Software Developer'

} 30

*/

ES5代码如下:

// ES5 Code

function  getUsers(page, results, gender, nationality)  {

  var params =  "";

  if(page ===  0  || page)  {

 params +=  `page=${page}&`;

  }

  if(results)  {

 params +=  `results=${results}&`;

  }

  if(gender)  {

 params +=  `gender=${gender}&`;

  }

  if(nationality)  {

 params +=  `nationality=${nationality}`;

  }

  fetch('https://randomuser.me/api/?'  + params)

  .then(function(response)  {

  return response.json();

  })

  .then(function(result)  {

 console.log(result);

  })

  .catch(function(error)  {

 console.log('error', error);

  });

}

getUsers(0,  10,  'male',  'us');

在这段代码中,我们通过在getUsers函数中传递各种可选参数来进行API调用。在进行API调用之前,我们添加了各种if条件来检查是否添加了参数,并基于此构造查询字符串,如下所示:

https://randomuser.me/api/? page=0&results=10&gender=male&nationality=us

使用ES6的默认参数则不必添这么多if条件,如下所示:

function  getUsers(page =  0, results =  10, gender =  'male',nationality =  'us')  {

 fetch(`https://randomuser.me/api/?page=${page}&results=${results}&gender=${gender}&nationality=${nationality}`)

 .then(function(response)  {

  return response.json();

 })

 .then(function(result)  {

 console.log(result);

 })

 .catch(function(error)  {

 console.log('error', error);

  });

}

getUsers();

这样一来,代码得到了大量的简化,即便我们不为getUsers函数提供任何参数时,它也能采用默认值。当然,我们也可以传递自己的参数:

getUsers(1,  20,  'female',  'gb');

它将覆盖函数的默认参数。

null不等于未定义

注意: 定义默认参数时,null和undefined是不同的。

我们来看下面的代码:

function  display(name =  'David', age =  35, location =  'NY'){

 console.log(name, age, location);

}

display('David',  35);  // David 35 NY

display('David',  35,  undefined);  // David 35 NY

// OR

display('David',  35,  undefined);  // David 35 NY

display('David',  35,  null);  // David 35 null

当我们传递null作为参数时,它实际是给location参数赋一个空值,与undefined不一样。所以它不会取默认值“NY”。</pre>

Array.prototype.includes

ES7增加了数组的includes方法,用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。

// ES5 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.indexOf("one") > -1); // true 
console.log(numbers.indexOf("five") > -1); // false

数组可以使用includes方法:

// ES7 Code

const numbers =  ["one",  "two",  "three",  "four"];

console.log(numbers.includes("one"));  // true

console.log(numbers.includes("five"));  // false

includes方法可以使代码简短且易于理解,它也可用于比较不同的值。

<pre style="line-height:18.0pt;
vertical-align:baseline">const day =  "monday";</pre>

if(day ===  "monday"  || day ===  "tuesday"  || day ===  "wednesday")  {

  // do something

}

// 以上代码使用include方法可以简化如下:

const day =  "monday";

if(["monday",  "tuesday",  "wednesday"].includes(day))  {

  // do something

}

因此,在检查数组中的值时,使用includes方法将会非常的方便。

支持ES6+ 的第三方组件

除了语法更新,开发者还需掌握一些第三方组件,以便更好的完成项目交付。

例如,使用 SpreadJS表格组件,可以向 JavaScript 应用程序添加高级电子表格功能,包括支持 450 多种计算公式、在线导入导出 Excel 文档、数据透视表和可视化分析,以创建财务、分析、预算/预测、数据收集、科学和许多其他类似的应用程序。

如何在 Vue中使用SpreadJS

SpreadJS可以通过以下两种方式与Vue一起使用:

  1. 使用Node包管理器:

打开命令提示符窗口并键入以下命令,使用vue init webpack创建一个简单的Vue项目:

$ npm install --global vue-cli

# create a new project using the "webpack" template
$ vue init webpack my-project

# install dependencies and go!
$ cd my-project
$ npm run dev         

在项目中导入SpreadJS Vue模块:

$ npm install @grapecity/spread-sheets-vue

在Vue应用程序中使用SpreadJS:

<template>

<div>

 <gc-spread-sheets

 :hostClass='hostClass'

 >

 <gc-worksheet

 :dataSource="dataTable"

 :autoGenerateColumns = 'autoGenerateColumns'

 >

 <gc-column

 :width="width"

 :dataField="'price'"

 :visible = 'visible'

 :formatter = 'formatter'

 :resizable = 'resizable'

 ></gc-column>

 </gc-worksheet>

 </gc-spread-sheets>

 </div>

</template>

<script>

import '@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'

import '@grapecity/spread-sheets-vue'

export default {

data(){

  return {

 hostClass:'spread-host',

 autoGenerateColumns:true,

 width:300,

 visible:true,

 resizable:true,

 formatter:"$ #.00"

 }

},

computed:{

 dataTable(){

 let dataTable = [];

  for (let i = 0; i < 42; i++) {

 dataTable.push({price: i + 0.56})

 }

  return dataTable

 }

}

}

</script>

<style scoped>

.spread-host {

width: 500px;

height: 600px;

}

</style>
  1. 使用传统HTML

创建HTML页面,将SpreadJS和Vue-SpreadJS添加到HTML模板:添加引用 gc.spread.sheets.all....min.js, gc.SpreadJS....css 和 gc.spread.sheets.vue...*.js 文件到 HTML 模板 (i.e. 你的 index.html 文件)即可。

点击此处,了解更多 SpreadJS 在Vue的技术资源

如何在 React等框架使用SpreadJS

与 Vue 类似,SpreadJS也可以通过使用Node包管理器和传统HTML 两种方式与React一起使用,点击此处,了解使用方式。

结语

从ES6开始,JavaScript中发生许多变更。对于JavaScript,Angular,React或Vue开发人员都应该知道它们。

了解这些变更可以使你成为更棒的开发者,甚至可以帮助您获得更高的薪水。而且,如果你只是在学习React之类的库以及Angular和Vue之类的框架,那么您一定要掌握这些新特性。

查看原文

赞 31 收藏 19 评论 0

zzard 赞了回答 · 2020-12-25

解决前端框架的作用是什么?什么时候需要用到前端框架?

他们出现的原因是什么?

当前端从 Web Page 变成了 Web App 时,就需要前端框架了。

有什么优点?

自行 Google。


更新

在评论中,@C860 说

就目前的WEB PAGE来说,引入前端框架已经是大势所趋了。很多时候后端的一些数据处理都转移给了前端去完成,特别是在REST模式下。

就他的描述来看,后端的一些数据处理都转移给了前端去完成 这绝对不是 Web Page,这明显已经是 Web App 了。

  • Web Page 以表现为主
  • Web App 以应用为主

现在我们在 web 上,已经不仅仅是去了,我们更多的时候是去

大公司有没有用我不敢轻易下结论,但是我知道 TX 他们有自己写的库

看来他还真是把 Web Page 和 Web App 理解错了,TX 的公司网站是 http://www.tencent.com 我不用看他的源码,就可以猜出,这么个 Web Page 型网站,不会去使用这些 MVC 框架的。如果他提到的 TX网站 是QQ空间、WebQQ,那就是 Web App 了。

更新2

也许很多人不太理解 Web Page 和 Web App 的区别。不仅仅是 Web,就连现在的桌面软件,手机软件(呃~我提到这个词会被行家笑话的,应该叫“移动App”、“手机应用”、“……”)。

为什么我们发明了这么个新名词呢?App。

如果你有使用 VC、VB、Delphi、PB 的开发经验,就会注意到我们建立新项目时,会有几个选择 Win32、Console、桌面软件、Lib、……

换句话说:“并不是所有的软件都是 App”,软件分很多种,如果我们写一个水仙花数的解法,那么,我们编写的是“程序program”,广义的说,这其实也勉强算是一个APP吧(你可以包装一下),因为他确实是一个应用,他可以用来解决我们的一个或者几个问题。

或者你把他再包装一下,写个文档,使用手册,……那么你可以说你开发了一个“软件software”。

如果你开发的是类库,比如你开发了一个 jquery,开发了一个 spring,那么即使你的程序编写的再好,他也不是一个 APP。

如果你开发了一个系统软件,比如编译器,比如驱动程序,比如文件系统……那么这个也不是APP。

那什么是 web app 呢?就是 Web 应用。(这不废话吗!)

Web 最初出现的时候,可以说仅仅是颠覆了传统传媒业,颠覆了报纸业,新闻业,…… 我们做了那么多网页(Web Page 翻译的多贴切,web就是网,page就是页)。

后来,java applet、flash、javascript、slivelight、flex……各种技术的出现,改变了人们发布和部署APP和software的方式,虽然悄无声息,但也绝对是软件发布方式的一次革命。

关注 8 回答 4

zzard 赞了回答 · 2020-11-10

解决怎样判断一个jquery对象是否为空jquery对象

也可以直接判断$('#myDiv')[0]===undefined,如果true的话说明没有。

关注 4 回答 4

zzard 收藏了文章 · 2020-10-13

浏览器是如何工作的:Chrome V8让你更懂JavaScript

Chrome V8
  V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行

上图清晰版

记得那年花下,深夜,初识谢娘时

为什么需要 JavaScript 引擎

  我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码
  JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收

# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000  #机器指令
mov ax,bx         #汇编指令
资料拓展: 汇编语言入门教程【阮一峰】

热门 JavaScript 引擎

  • V8 (Google),用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
  • JavaScriptCore (Apple),开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
  • Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit
  • SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
  • Chakra (JScript 引擎),用于 Internet Explorer。
  • Chakra (JavaScript 引擎),用于 Microsoft Edge。
  • KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波顿开发,用于 KDE 项目的 Konqueror 网页浏览器中。
  • JerryScript — 三星推出的适用于嵌入式设备的小型 JavaScript 引擎。
  • 其他:Nashorn、QuickJSHermes

V8

  Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。

  和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作

how-v8-works

资料拓展:v8 logo | V8 (JavaScript engine)) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly

V8一词最早见于“V-8 engine”,即V8发动机,一般使用在中高端车辆上。8个气缸分成两组,每组4个,成V型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar和NASCAR都要求使用V8发动机。

与君初相识,犹如故人归

什么是 D8

  d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息

V8源码编译出来的可执行程序名为d8。d8作为V8引擎在命令行中可以使用的交互shell存在。Google官方已经不记得d8这个名字的由来,但是做为"delveloper shell"的缩写,用首字母d和8结合,恰到好处。
还有一种说法是d8最初叫developer shell,因为d后面有8个字符,因此简写为d8,类似于i18n(internationalization)这样的简写。
参考:Using d8

安装 D8

  • 方法一:自行下载编译

  • 方法二:使用编译好的 d8 工具

    // 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开)
      V8 version 8.4.109
      d8> 1 + 2
        3
      d8> 2 + '4'
        "24"
      d8> console.log(23)
        23
        undefined
      d8> var a = 1
        undefined
      d8> a + 2
        3
      d8> this
        [object global]
      d8>

    本文后续用于 demo 演示时的文件目录结构:

     V8:
        # d8可执行文件
        d8
        icudtl.dat
        libc++.dylib
        libchrome_zlib.dylib
        libicui18n.dylib
        libicuuc.dylib
        libv8.dylib
        libv8_debug_helper.dylib
        libv8_for_testing.dylib
        libv8_libbase.dylib
        libv8_libplatform.dylib
        obj
        snapshot_blob.bin
        v8_build_config.json
        # 新建的js示例文件
        test.js
    • 方法三:mac

        # 如果已有HomeBrew,忽略第一条命令
        ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
        brew install v8
    • 方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解释器)生成的 Bytecode(字节码)。

都有哪些 d8 命令可供使用?

  • 查看 d8 命令

      # 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
      ./d8 --help
  • 过滤特定的命令

      # 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
      ./d8 --help |grep print

    如:

    • print-bytecode 查看生成的字节码
    • print-opt-code 查看优化后的代码
    • print-ast 查看中间生成的 AST
    • print-scopes 查看中间生成的作用域
    • trace-gc 查看这段代码的内存回收状态
    • trace-opt 查看哪些代码被优化了
    • trace-deopt 查看哪些代码被反优化了
    • turbofan-stats 打印优化编译器的一些统计数据

使用 d8 进行调试

// test.js
function sum(a) {
  var b = 6;
  return a + 6;
}
console.log(sum(3));
  # d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。
  ./d8 ./test.js --print-bytecode
  # 执行以下命令,输出9
  ./d8 ./test.js

内部方法

  你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax 命令,你就可以在 test.js 中使用诸如HasFastProperties(检查一个对象是否拥有快属性)的内部方法(索引属性、常规属性、快属性等下文会介绍)。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);
// 检查一个对象是否拥有快属性
console.log(%HasFastProperties(bar));
delete bar.property2;
console.log(%HasFastProperties(bar));
  ./d8 --allow-natives-syntax ./test.js
  # 依次打印:true false

心似双丝网,中有千千结

V8 引擎的内部结构

  V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:

  • Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)

    确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:
    Scanner
  • Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。

    通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
    基于寄存器的解释器架构基于寄存器的解释器架构
    资料参考:解释器是如何解释执行字节码的?
  • TurboFan:compiler,即编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。

  其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
V8流程
  简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能。

  图片中的红色虚线是逆向的,也就是说Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。

function add(x, y) {
  return x + y;
}

add(3, 5);
add('3', '5');

  在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器

资料拓展参考:V8 引擎是如何工作的?

V8 是怎么执行一段 JavaScript 代码的

  • V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了即时编译(JIT)双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。V8 出现之后,各大厂商也都在自己的 JavaScript 虚拟机中引入了 JIT 机制,所以目前市面上 JavaScript 虚拟机都有着类似的架构。另外,V8 也是早于其他虚拟机引入了惰性编译、内联缓存、隐藏类等机制,进一步优化了 JavaScript 代码的编译执行效率
  • V8 执行一段 JavaScript 的流程图:
    V8执行一段JavaScript流程图

    资料拓展:V8 是如何执行一段 JavaScript 代码的?
  • V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:

    • 第一种是将高级代码转换为二进制代码,再让计算机去执行;
    • 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。
  • 解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码
  • 总结:

    V8 执行一段 JavaScript 代码所经历的主要流程包括:

    • 初始化基础环境;
    • 解析源码生成 AST 和作用域;
    • 依据 AST 和作用域生成字节码;
    • 解释执行字节码;
    • 监听热点代码;
    • 优化热点代码为二进制的机器代码;
    • 反优化生成的二进制机器代码。

一等公民与闭包

一等公民的定义

  • 在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
  • 如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。对于各种编程语言来说,函数就不一定是一等公民了,比如 Java 8 之前的版本。
  • 对于 JavaScript 来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此 JavaScript 中函数是一等公民

动态作用域与静态作用域

  • 如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量。即静态作用域可以由程序代码决定,在编译时就能完全确定。大多数语言都是静态作用域的。
  • 动态作用域(Dynamic Scope)。也就是说,变量引用跟变量声明不是在编译时就绑定死了的。在运行时,它是在运行环境中动态地找一个相同名称的变量。在 macOS 或 Linux 中用的 bash 脚本语言,就是动态作用域的。

闭包的三个基础特性

  • JavaScript 语言允许在函数内部定义新的函数
  • 可以在内部函数中访问父函数中定义的变量
  • 因为 JavaScript 中的函数是一等公民,所以函数可以作为另外一个函数的返回值
// 闭包(静态作用域,一等公民,调用栈的矛盾体)
function foo() {
  var d = 20;
  return function inner(a, b) {
    const c = a + b + d;
    return c;
  };
}
const f = foo();

  关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。

惰性解析

  所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。

  • 在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,这主要是基于以下两点:

    • 首先,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。因为有时候一个页面的 JavaScript 代码很大,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间;
    • 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直占用内存
  • 基于以上的原因,所有主流的 JavaScript 虚拟机都实现了惰性解析。
  • 闭包给惰性解析带来的问题:上文的 d 不能随着 foo 函数的执行上下文被销毁掉。

预解析器

  V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。

  • 判断当前函数是不是存在一些语法上的错误,发现了语法错误,那么就会向 V8 抛出语法错误;
  • 检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题

V8 内部是如何存储对象的:快属性和慢属性

下面的代码会输出什么:

// test.js
function Foo() {
  this[200] = 'test-200';
  this[1] = 'test-1';
  this[100] = 'test-100';
  this['B'] = 'bar-B';
  this[50] = 'test-50';
  this[9] = 'test-9';
  this[8] = 'test-8';
  this[3] = 'test-3';
  this[5] = 'test-5';
  this['D'] = 'bar-D';
  this['C'] = 'bar-C';
}
var bar = new Foo();

for (key in bar) {
  console.log(`index:${key}  value:${bar[key]}`);
}
//输出:
// index:1  value:test-1
// index:3  value:test-3
// index:5  value:test-5
// index:8  value:test-8
// index:9  value:test-9
// index:50  value:test-50
// index:100  value:test-100
// index:200  value:test-200
// index:B  value:bar-B
// index:D  value:bar-D
// index:C  value:bar-C

  在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);

  可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。(Console面板运行以上代码,打开Memory面板,通过点击Take heap snapshot记录内存快照,点击快照,筛选出Foo进行查看。可参考使用 chrome-devtools Memory 面板了解Memory面板。)

  我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

v8 属性存储:
v8属性存储

总结:

  因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。

  通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。

  但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度

资料拓展:快属性和慢属性:V8 是怎样提升对象属性访问速度的?

堆空间和栈空间

栈空间

  • 现代语言都是基于函数的,每个函数在执行过程中,都有自己的生命周期和作用域,当函数执行结束时,其作用域也会被销毁,因此,我们会使用栈这种数据结构来管理函数的调用过程,我们也把管理函数调用过程的栈结构称之为调用栈
  • 栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等都会存在在栈上。当一个函数执行结束,那么该函数的执行上下文便会被销毁掉。
  • 栈空间的最大的特点是空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率非常高,但是通常在内存中,很难分配到一块很大的连续空间,因此,V8 对栈空间的大小做了限制,如果函数调用层过深,那么 V8 就有可能抛出栈溢出的错误。
  • 栈的优势和缺点:

    • 栈的结构非常适合函数调用过程。
    • 在栈上分配资源和销毁资源的速度非常快,这主要归结于栈空间是连续的,分配空间和销毁空间只需要移动下指针就可以了。
    • 虽然操作速度非常快,但是栈也是有缺点的,其中最大的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配一块连续的大空间是非常难的,因此栈空间是有限的

      // 栈溢出
      function factorial(n) {
        if (n === 1) {
          return 1;
        }
        return n * factorial(n - 1);
      }
      console.log(factorial(50000));

堆空间

  • 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。
  • 宿主在启动 V8 的过程中,会同时创建堆空间和栈空间,再继续往下执行,产生的新数据都会存放在这两个空间中。

继承

  继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性

  JavaScript 的每个对象都包含了一个隐藏属性 __proto__ ,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。

  JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。

隐藏属性__proto__

var animal = {
  type: 'Default',
  color: 'Default',
  getInfo: function () {
    return `Type is: ${this.type},color is ${this.color}.`;
  },
};
var dog = {
  type: 'Dog',
  color: 'Black',
};

利用__proto__实现继承:

dog.__proto__ = animal;
dog.getInfo();

  通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__,但是在实际项目中,我们不应该直接通过 __proto__ 来访问或者修改该属性,其主要原因有两个:

  • 首先,这是隐藏属性,并不是标准定义的;
  • 其次,使用该属性会造成严重的性能问题。因为 JavaScript 通过隐藏类优化了很多原有的对象结构,所以通过直接修改__proto__会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!

构造函数是怎么创建对象的?

  在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。

function DogFactory(type, color) {
  this.type = type;
  this.color = color;
}
var dog = new DogFactory('Dog', 'Black');

  其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:

var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, 'Dog', 'Black');

机器码、字节码

V8 为什么要引入字节码

  • 早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
  • 随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:

    • 时间问题:编译时间过久,影响代码启动速度;
    • 空间问题:缓存编译后的二进制代码占用更多的内存。
  • 这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:

    • 解决启动问题:生成字节码的时间很短;
    • 解决空间问题:字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码还是小了太多,缓存字节码会大大降低内存的使用。
    • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。
  • Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
  • 如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。

如何查看字节码

// test.js
function add(x, y) {
  var z = x + y;
  return z;
}
console.log(add(1, 2));

运行./d8 ./test.js --print-bytecode:

[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
         0x10008250026 @    0 : 25 02             Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register
         0x10008250028 @    2 : 34 03 00          Add a0, [0]
         0x1000825002b @    5 : 26 fb             Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
         0x1000825002d @    7 : aa                Return  #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3

常用字节码指令

  • Ldar:表示将寄存器中的值加载到累加器中,你可以把它理解为 LoaD Accumulator from Register,就是把某个寄存器中的值,加载到累加器中。
  • Star:表示 Store Accumulator Register, 你可以把它理解为 Store Accumulator to Register,就是把累加器中的值保存到某个寄存器中
  • Add:Add a0, [0]是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。

    add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
  • LdaSmi:将小整数(Smi)加载到累加器寄存器中
  • Return:结束当前函数的执行,并将控制权传回给调用方。返回的值是累加器中的值。

bytecode-ignition

V8 中的字节码指令集 | 理解 V8 的字节码「译」

隐藏类和内联缓存

  JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存

为什么静态语言的效率更高?

  静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。

  JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。

将静态的特性引入到 V8

  • V8 采用的一个思路就是将 JavaScript 中的对象静态化,也就是 V8 在运行 JavaScript 的过程中,会假设 JavaScript 中的对象是静态的。
  • 具体地讲,V8 对每个对象做如下两点假设:

    • 对象创建好了之后就不会添加新的属性;
    • 对象创建好了之后也不会删除属性。
  • 符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:

    • 对象中所包含的所有的属性;
    • 每个属性相对于对象的偏移量。
  • 有了隐藏类之后,那么当 V8 访问某个对象中的某个属性时,就会先去隐藏类中查找该属性相对于它的对象的偏移量,有了偏移量和属性类型,V8 就可以直接去内存中取出对应的属性值,而不需要经历一系列的查找过程,那么这就大大提升了 V8 查找对象的效率。
  • 在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类;
  • map 描述了对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少。

通过 d8 查看隐藏类

// test.js
let point1 = { x: 100, y: 200 };
let point2 = { x: 200, y: 300 };
let point3 = { x: 100 };
%DebugPrint(point1);
%DebugPrint(point2);
%DebugPrint(point3);
 ./d8 --allow-natives-syntax ./test.js
# ===============
DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
# V8 为 point1 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
    #y: 200 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
# V8 为 point2 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 200 (const data field 0)
    #y: 300 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
# V8 为 point3 对象创建的隐藏类
 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
 }
0x1ea308284d39: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

多个对象共用一个隐藏类

  • 在 V8 中,每个对象都有一个 map 属性,该属性值指向该对象的隐藏类。不过如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,这样有两个好处:

    • 减少隐藏类的创建次数,也间接加速了代码的执行速度;
    • 减少了隐藏类的存储空间。
  • 那么,什么情况下两个对象的形状是相同的,要满足以下两点:

    • 相同的属性名称;
    • 相等的属性个数。

重新构建隐藏类

  • 给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么势必也就会触发 V8 为改变形状后的对象重建新的隐藏类。
// test.js
let point = {};
%DebugPrint(point);
point.x = 100;
%DebugPrint(point);
point.y = 200;
%DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...
  • 每次给对象添加了一个新属性之后,该对象的隐藏类的地址都会改变,这也就意味着隐藏类也随着改变了;如果删除对象的某个属性,那么对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类;
  • 最佳实践

    • 使用字面量初始化对象时,要保证属性的顺序是一致的;
    • 尽量使用字面量一次性初始化完整对象属性;
    • 尽量避免使用 delete 方法。

通过内联缓存来提升函数执行效率

  虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:

function loadX(obj) {
  return obj.x;
}
var obj = { x: 1, y: 3 };
var obj1 = { x: 3, y: 6 };
var obj2 = { x: 3, y: 6, z: 8 };
for (var i = 0; i < 100; i++) {
  // 对比时间差异
  console.time(`---${i}----`)
  loadX(obj);
  console.timeEnd(`---${i}----`)
  loadX(obj1);
  // 产生多态
  loadX(obj2);
}

通常 V8 获取 obj.x 的流程

  • 找对象 obj 的隐藏类;
  • 再通过隐藏类查找 x 属性偏移量;
  • 然后根据偏移量获取属性值,在这段代码中 loadX 函数会被反复执行,那么获取 obj.x 的流程也需要反复被执行;

内联缓存及其原理

  • 函数 loadX 在一个 for 循环里面被重复执行了很多次,因此 V8 会想尽一切办法来压缩这个查找过程,以提升对象的查找效率。这个加速函数执行的策略就是内联缓存 (Inline Cache),简称为 IC;
  • IC 的原理:在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。
  • IC 会为每个函数维护一个反馈向量 (FeedBack Vector),反馈向量记录了函数在执行过程中的一些关键的中间数据。
  • 反馈向量其实就是一个表结构,它由很多项组成的,每一项称为一个插槽 (Slot),V8 会依次将执行 loadX 函数的中间数据写入到反馈向量的插槽中。
  • 当 V8 再次调用 loadX 函数时,比如执行到 loadX 函数中的 return obj.x 语句时,它就会在对应的插槽中查找 x 属性的偏移量,之后 V8 就能直接去内存中获取 obj.x 的属性值了。这样就大大提升了 V8 的执行效率。

单态、多态和超态

  • 如果一个插槽中只包含 1 个隐藏类,那么我们称这种状态为单态 (monomorphic);
  • 如果一个插槽中包含了 2 ~ 4 个隐藏类,那我们称这种状态为多态 (polymorphic);
  • 如果一个插槽中超过 4 个隐藏类,那我们称这种状态为超态 (magamorphic)。
  • 单态的性能优于多态和超态,所以我们需要稍微避免多态和超态的情况。要避免多态和超态,那么就尽量默认所有的对象属性是不变的,比如你写了一个 loadX(obj) 的函数,那么当传递参数时,尽量不要使用多个不同形状的 obj 对象。

总结:
  V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况

异步编程与消息队列

V8 是如何执行回调函数的

  回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。
  通用 UI 线程宏观架构:
通用UI线程架构
  UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:

  • setTimeout 的执行流程其实是比较简单的,在 setTimeout 函数内部封装回调消息,并将回调消息添加进消息队列,然后主线程从消息队列中取出回调事件,并执行回调函数。
  • XMLHttpRequest 稍微复杂一点,因为下载过程需要放到单独的一个线程中去执行,所以执行 XMLHttpRequest.send 的时候,宿主会将实际请求转发给网络线程,然后 send 函数退出,主线程继续执行下面的任务。网络线程在执行下载的过程中,会将一些中间信息和回调函数封装成新的消息,并将其添加进消息队列中,然后主线程从消息队列中取出回调事件,并执行回调函数。

宏任务和微任务

  • 调用栈:调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。主线程在执行任务的过程中,如果函数的调用层次过深,可能造成栈溢出的错误,我们可以使用 setTimeout 来解决栈溢出的问题。setTimeout 的本质是将同步函数调用改成异步函数调用,这里的异步调用是将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程再按照一定规则循环地从消息队列中读取下一个宏任务。
  • 宏任务:就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。
  • 微任务:你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
  • JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
  • 微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。

    微任务技术栈

    // 不会使浏览器卡死
    function foo() {
      setTimeout(foo, 0);
    }
    foo();

    调用栈、主线程、消息队列
    微任务:

// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
  return Promise.resolve().then(foo);
}
foo();
  • 如果当前的任务中产生了一个微任务,通过 Promise.resolve() 或者 Promise.reject() 都会触发微任务,触发的微任务不会在当前的函数中被执行,所以执行微任务时,不会导致栈的无限扩张
  • 和异步调用不同,微任务依然会在当前任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。因此在函数内部触发的微任务,一定比在函数内部触发的宏任务要优先执行。
  • 微任务依然是在当前的任务中执行的,所以如果在微任务中循环触发新的微任务,那么将导致消息队列中的其他任务没有机会被执行。

前端异步编程方案史

前端异步编程方案史

  • Callback 模式的异步编程模型需要实现大量的回调函数,大量的回调函数会打乱代码的正常逻辑,使得代码变得不线性、不易阅读,这就是我们所说的回调地狱问题
  • Promise 能很好地解决回调地狱的问题,我们可以按照线性的思路来编写代码,这个过程是线性的,非常符合人的直觉。
  • 但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着大量的 then,语义化不明显,代码不能很好地表示执行流程。我们想要通过线性的方式来编写异步代码,要实现这个理想,最关键的是要能实现函数暂停和恢复执行的功能。而生成器就可以实现函数暂停和恢复,我们可以在生成器中使用同步代码的逻辑来异步代码 (实现该逻辑的核心是协程)。
  • 但是在生成器之外,我们还需要一个触发器来驱动生成器的执行。前端的最终方案就是 async/await,async 是一个可以暂停和恢复执行的函数,在 async 函数内部使用 await 来暂停 async 函数的执行,await 等待的是一个 Promise 对象,如果 Promise 的状态变成 resolve 或者 reject,那么 async 函数会恢复执行。因此,使用 async/await 可以实现以同步的方式编写异步代码这一目标。和生成器函数一样,使用了 async 声明的函数在执行时,也是一个单独的协程,我们可以使用 await 来暂停该协程,由于 await 等待的是一个 Promise 对象,我们可以 resolve 来恢复该协程。
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

资料拓展:co 函数库的含义和用法

垃圾回收

垃圾数据

  从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。

垃圾回收算法

垃圾回收大致可以分为以下几个步骤:

  • 第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:

    • 通过 GC Root 遍历到的对象,我们就认为该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,我们也称可访问的对象为活动对象
    • 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,我们称不可访问的对象为非活动对象
    • 浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):

      • 全局的 window 对象(位于每个 iframe 中);
      • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
      • 存放栈上变量。
  • 第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
  • 第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)

垃圾回收

  • V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。代际假说有两个特点:

    • 第一个是大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的时间很短,比如函数内部声明的变量,或者块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁。因此这一类对象一经分配内存,很快就变得不可访问;
    • 第二个是不死的对象,会活得更久,比如全局的 window、DOM、Web API 等对象。
  • 为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。

    • 主垃圾回收器负责收集老生代中的垃圾数据,副垃圾回收器负责收集新生代中的垃圾数据。
    • 副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域(有些地方也称作From和To空间),一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。

      • 这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
      • 副垃圾回收器每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域,复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小
      • 副垃圾回收器还会采用对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到老生代中。
    • 主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程

      • 主垃圾回收器主要负责老生代中的垃圾回收。除了新生代中晋升的对象,一些大的对象会直接被分配到老生代里。
      • 老生代中的对象有两个特点:一个是对象占用空间大;另一个是对象存活时间长。

Stop-The-World

  由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)

  • V8 最开始的垃圾回收器有两个特点:

    • 第一个是垃圾回收在主线程上执行,
    • 第二个特点是一次执行一个完整的垃圾回收流程。
  • 由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案。

    • 第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。
    • 第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。
    • 第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。

      资料参考:深入解读 V8 引擎的「并发标记」技术
    • 主垃圾回收器就综合采用了所有的方案(并发标记,增量标记,辅助清理),副垃圾回收器也采用了部分方案。

似此星辰非昨夜,为谁风露立中宵

Breaking the JavaScript Speed Limit with V8

  Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的JavaScript代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧

  1. 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类);
  2. 总是以相同的次序初始化对象成员;
  3. 尽量使用可以用 31 位有符号整数表示的数;
  4. 为数组使用从 0 开始的连续的主键;
  5. 别预分配大数组(比如大于 64K 个元素)到其最大尺寸,令其尺寸顺其自然发展就好;
  6. 别删除数组里的元素,尤其是数字数组;
  7. 别加载未初始化或已删除的元素;
  8. 对于固定大小的数组,使用”array literals“初始化(初始化小额定长数组时,用字面量进行初始化);
  9. 小数组(小于 64k)在使用之前先预分配正确的尺寸;
  10. 请勿在数字数组中存放非数字的值(对象);
  11. 尽量使用单一类型(monomorphic)而不是多类型(polymorphic)(如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换);
  12. 不要使用 try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);
  13. 在优化后避免在方法中修改隐藏类。
演讲资料参考: Performance Tips for JavaScript in V8 | JavaScript V8性能小贴士【译】 | 内网视频 | YouTube

在 V8 引擎里 5 个优化代码的技巧

  1. 对象属性的顺序: 在实例化你的对象属性的时候一定要使用相同的顺序,这样隐藏类和随后的优化代码才能共享;
  2. 动态属性: 在对象实例化之后再添加属性会强制使得隐藏类变化,并且会减慢为旧隐藏类所优化的代码的执行。所以,要在对象的构造函数中完成所有属性的分配;
  3. 方法: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存);
  4. 数组: 避免使用 keys 不是递增的数字的稀疏数组,这种 key 值不是递增数字的稀疏数组其实是一个 hash 表。在这种数组中每一个元素的获取都是昂贵的代价。同时,要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后,不要删除数组中的元素,因为这会使得 keys 变得稀疏;
  5. 标记值 (Tagged values): V8 用 32 位来表示对象和数字。它使用一位来区分它是对象 (flag = 1) 还是一个整型 (flag = 0),也被叫做小整型(SMI),因为它只有 31 位。然后,如果一个数值大于 31 位,V8 将会对其进行 box 操作,然后将其转换成 double 型,并且创建一个新的对象来装这个数。所以,为了避免代价很高的 box 操作,尽量使用 31 位的有符号数。
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈JavaScript中装箱和拆箱

JavaScript 启动性能瓶颈分析与解决方案

资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案

天长地久有时尽,V8 绵绵无绝期

资料参考

番外篇

  • Chrome插件Console Importer推荐:Easily import JS and CSS resources from Chrome console. (可以在浏览器控制台安装 loadsh、moment、jQuery 等库,在控制台直接验证、使用这些库。)
    效果图:
    BCv8tP.gif

本文首发于个人博客,欢迎指正和star
本文同步发布并于掘金社区

查看原文

zzard 赞了文章 · 2020-07-06

JavaScript闭包的应用

本文最先发布于我的个人网站:https://wintc.top/article/33。转载请注明出处。

  本文介绍一下JS中的一个重要概念——闭包。其实即便是最初级的前端开发人员,应该都已经接触过它。

一、闭包的概念和特性

  首先看个闭包的例子:

function makeFab () {
  let last = 1, current = 1
  return function inner() {
    [current, last] = [current + last, current]
    return last
  }
}

let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5

  这是一个生成斐波那契数列的例子。makeFab的返回值就是一个闭包,makeFab像一个工厂函数,每次调用都会创建一个闭包函数,如例子中的fab。fab每次调用不需要传参数,都会返回不同的值,因为在闭包生成的时候,它记住了变量last和current,以至于在后续的调用中能够返回不同的值。能记住函数本身所在作用域的变量,这就是闭包和普通函数的区别所在。

  MDN中给出的闭包的定义是:函数与对其状态即词法环境的引用共同构成闭包。这里的“词法环境的引用”,可以简单理解为“引用了函数外部的一些变量”,例如上述例子中每次调用makeFab都会创建并返回inner函数,引用了last和current两个变量。

二、闭包——函数式编程之魂

Javascript和python这两门动态语言都强调一个概念:万物皆对象。自然,函数也是对象。
在Javascript里,我们可以像操作普通变量一样,把函数在我们的代码里抛来抛去,然后在某个时刻调用一下,这就是所谓的函数式编程。函数式编程灵活简洁,而语言对闭包的支持,让函数式编程拥有了灵魂。

以实现一个可复用的确认框为例,比如在用户进行一些删除或者重要操作的时候,为了防止误操作,我们可能会通过弹窗让用户再次确认操作。因为确认框是通用的,所以确认框组件的逻辑应该足够抽象,仅仅是负责弹窗、触发确认、触发取消事件,而触发确认/取消事件是异步操作,这时候我们就需要使用两个回调函数完成操作,弹窗函数confirm接收三个参数:一个提示语句,一个确认回调函数,一个取消回调函数:

function confirm (confirmText, confirmCallback, cancelCallback) {
  // 插入提示框DOM,包含提示语句、确认按钮、取消按钮
  // 添加确认按钮点击事件,事件函数中做dom清理工作并调用confirmCallback
  // 添加取消按钮点击事件,事件函数中做dom清理工作并调用cancelCallback
}

这样我们可以通过向confirm传递回调函数,并且根据不同结果完成不同的动作,比如我们根据id删除一条数据可以这样写:

function removeItem (id) {
  confirm('确认删除吗?', () => {
    // 用户点击确认, 发送远程ajax请求
    api.removeItem(id).then(xxx)
  }, () => {
    // 用户点击取消,
    console.log('取消删除')
  })
}

这个例子中,confirmCallback正是利用了闭包,创建了一个引用了上下文中id变量的函数,这样的例子在回调函数中比比皆是,并且大多数时候引用的变量是很多个。 试想,如果语言不支持闭包,那这些变量要怎么办?作为参数全部传递给confirm函数,然后在调用confirmCallback/cancelCallback时再作为参数传递给它们?显然,这里闭包提供了极大便利。

三、闭包的一些例子

1. 防抖、节流函数

  前端很常见的一个需求是远程搜索,根据用户输入框的内容自动发送ajax请求,然后从后端把搜索结果请求回来。为了简化用户的操作,有时候我们并不会专门放置一个按钮来点击触发搜索事件,而是直接监听内容的变化来搜索(比如像vue的官网搜索栏)。这时候为了避免请求过于频繁,我们可能就会用到“防抖”的技巧,即当用户停止输入一段时间(比如500ms)后才执行发送请求。可以写一个简单的防抖函数实现这个功能:

function debounce (func, time) {
  let timer = 0
  return function (...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      timer = 0
      func.apply(this, args)
    }, time)
  }
}

input.onkeypress = debounce(function () {
  console.log(input.value) // 事件处理逻辑
}, 500)

  debounce函数每次调用时,都会创建一个新的闭包函数,该函数保留了对事件逻辑处理函数func以及防抖时间间隔time以及定时器标志timer的引用。类似的还有节流函数:

function throttle(func, time) {
  let timer = 0 // 定时器标记相当于一个锁标志
  return function (...args) {
    if (timer) return
    func.apply(this, args)
    timer = setTimeout(() => timer = 0, time)
  }
}

2. 优雅解决按钮多次连续点击问题

        用户点击一个表单提交按钮,前端会向后台发送一个异步请求,请求还没返回,焦急的用户又多点了几下按钮,造成了额外的请求。有时候多发几次请求最多只是多消耗了一些服务器资源,而另外一些情况是,表单提交本身会修改后台的数据,那多次提交就会导致意料之外的后果了。无论是为了减少服务器资源消耗还是避免多次修改后台数据,给表单提交按钮添加点击限制是很有必要的。

        怎么解决呢?一个常用的办法是打个标记,即在响应函数所在作用域声明一个布尔变量lock,响应函数被调用时,先判断lock的值,为true则表示上一次请求还未返回,此次点击无效;为false则将lock设置为true,然后发送请求,请求结束再将lock改为false。

        很显然,这个lock会污染函数所在的作用域,比如在Vue组件中,我们可能就要将这个标记记录在组件属性上;而当有多个这样的按钮,则还需要不同的属性来标记(想想给这些属性取名都是一件头疼的事情吧!)。而生成闭包伴随着新的函数作用域的创建,利用这一点,刚好可以解决这个问题。下面是一个简单的例子:       

let clickButton = (function () {
  let lock = false
  return function (postParams) {
    if (lock) return
    lock = true
    // 使用axios发送请求
    axios.post('urlxxx', postParams).then(
      // 表单提交成功
    ).catch(error => {
      // 表单提交出错
      console.log(error)
    }).finally(() => {
      // 不管成功失败 都解锁
      lock = false
    })
  }
})()

button.addEventListener('click', clickButton)

        这样lock变量就会在一个单独的作用域里,一次点击的请求发出以后,必须等请求回来,才会开始下一次请求。

        当然,为了避免各个地方都声明lock,修改lock,我们可以把上述逻辑抽象一下,实现一个装饰器,就像节流/防抖函数一样。以下是一个通用的装饰器函数:

function singleClick(func, manuDone = false) {
  let lock = false
  return function (...args) {
    if (lock) return
    lock = true
    let done = () => lock = false
    if (manuDone) return func.call(this, ...args, done)
    let promise = func.call(this, ...args)
    promise ? promise.finally(done) : done()
    return promise
  }
}

        默认情况下,需要原函数返回一个promise以达到promise决议后将lock重置为false,而如果没有返回值,lock将会被立即重置(比如表单验证不通过,响应函数直接返回),调用示例:

let clickButton = singleClick(function (postParams) {
  if (!checkForm()) return
  return axios.post('urlxxx', postParams).then(
    // 表单提交成功
  ).catch(error => {
    // 表单提交出错
    console.log(error)
  })
})
button.addEventListener('click', clickButton)

        在一些不方便返回promise或者请求结束还要进行其它动作之后才能重置lock的地方,singleClick提供了第二个参数manuDone,允许你可以手动调用一个done函数来重置lock,这个done函数会放在原函数参数列表的末尾。使用例子:

let print = singleClick(function (i, done) {
  console.log('print is called', i)
  setTimeout(done, 2000)
}, true)

function test () {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      print(i)
    }, i * 1000)
  }
}

        print函数使用singleClick装饰,每次调用2秒后重置lock变量,测试每秒调用一次print函数,执行代码输出如下图:

       可以看到,其中一些调用没有打印结果,这正是我们想要的结果!singleClick装饰器比每次设置lock变量要方便许多,这里singleClick函数的返回值,以及其中的done函数,都是一个闭包。

3. 闭包模拟私有方法或者变量

  “封装”是面向对象的特性之一,所谓“封装”,即一个对象对外隐藏了其内部的一些属性或者方法的实现细节,外界仅能通过暴露的接口操作该对象。JS是比较“自由”的语言,所以并没有类似C++语言那样提供私有变量或成员函数的定义方式,不过利用闭包,却可以很好地模拟这个特性。

  比如游戏开发中,玩家对象身上通常会有一个经验属性,假设为exp,"打怪"、“做任务”、“使用经验书”等都会增加exp这个值,而在升级的时候又会减掉exp的值,把exp直接暴露给各处业务来操作显然是很糟糕的。在JS里面我们可以用闭包把它隐藏起来,简单模拟如下:

function makePlayer () {
  let exp = 0 // 经验值
  return {
    getExp () {
      return exp
    },
    changeExp (delta, sReason = '') {
      // log(xxx),记录变动日志
      exp += delta
    }
  }
}

let p = makePlayer()
console.log(p.getExp()) // 0
p.changeExp(2000)
console.log(p.getExp()) // 2000

  这样我们调用makePlayer()就会生成一个玩家对象p,p内通过方法操作exp这个变量,但是却不可以通过p.exp访问,显然更符合“封装”的特性。

四、总结

        闭包是JS中的强大特性之一,然而至于闭包怎么使用,我觉得不算是一个问题,甚至我们完全没必要研究闭包怎么使用。我的观点是,闭包应该是自然而言地出现在你的代码里,因为它是解决当前问题最直截了当的办法;而当你刻意想去使用它的时候,往往可能已经走了弯路。

查看原文

赞 14 收藏 11 评论 0

zzard 收藏了文章 · 2020-06-28

你不知道的 Web Workers (上)[7.8K 字 | 多图预警]

阅读完本文你将学到以下知识:

  • 进程与线程的区别:进程与线程的概念及单线程与多线程;
  • 浏览器内核的相关知识:GUI 渲染线程、JavaScript 引擎线程、事件触发线程等;
  • Web Workers 是什么:Web Workers 的限制与能力及主线程与 Web Workers 之间如何通信;
  • Web Workers 的分类:Dedicated Worker、Shared Worker 和 Service Workers;
  • Web Workers API:Worker 构造函数及如何观察 Dedicated Worker 等。

下面我们开始步入正题,为了让大家能够更好地理解和掌握 Web Workers,在正式介绍 Web Workers 之前,我们先来介绍一些与 Web Workers 相关的基础知识。

一、进程与线程的区别

在介绍进程与线程的概念前,我们先来看个进程与线程之间关系形象的比喻:

如上图所示,进程是一个工厂,它有独立的资源,线程是工厂中的工人,多个工人协作完成任务,工人之间共享工厂内的资源,比如工厂内的食堂或餐厅。此外,工厂(进程)与工厂(进程)之间是相互独立的。为了让大家能够更直观地理解进程与线程的区别,我们继续来看张图:

由上图可知,操作系统会为每个进程分配独立的内存空间,一个进程由一个或多个线程组成,同个进程下的各个线程之间共享程序的内存空间。相信通过前面两张图,小伙伴们对进程和线程之间的区别已经有了一定的了解,那么实际情况是不是这样呢?这里我们打开 macOS 操作系统下的活动监视器,来看一下写作本文时所有进程的状态:

通过上图可知,我们常用的软件,比如微信和搜狗输入法都是一个独立的进程,拥有不同的 PID(进程 ID),而且图中的每个进程都含有多个线程,以微信进程为例,它就含有 36 个线程。那么什么是进程和线程呢?下面我们来介绍进程和线程的概念。

1.1 进程的概念

进程(英语:process),是指计算机中已运行的程序。进程曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的 UNIX,Linux 2.4 及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6 及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步或异步的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时运行的感觉。

1.2 线程的概念

线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如 Win32 线程;由用户进程自行调度的用户线程,如 Linux 平台的 POSIX Thread;或者由内核与用户进程,如 Windows 7 的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。 但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。

1.3 单线程与多线程

如果一个进程只有一个线程,我们称之为单线程。单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程处理的优点:同步应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。

如果完成同步任务所用的时间比预计时间长,应用程序可能会不响应。针对这个问题,我们可以考虑使用多线程,即在进程中使用多个线程,这样就可以处理多个任务。

对于 Web 开发者熟悉的 JavaScript 来说,它运行在浏览器中,是单线程的,每个窗口一个 JavaScript 线程,既然是单线程的,在某个特定的时刻,只有特定的代码能够被执行,其它的代码会被阻塞。

JS 中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS 的设计初衷就没有考虑这些,针对 JS 这种不具备并行任务处理的特性,我们称之为 “单线程”。 —— 来自知乎 “如何证明 JavaScript 是单线程的?” @云澹的回答

其实在浏览器内核(渲染进程)中除了 JavaScript 引擎线程之外,还含有 GUI 渲染线程、事件触发线程、定时触发器线程等。因此对于浏览器的渲染进程来说,它是多线程的。接下来我们来简单介绍浏览器内核。

二、浏览器内核

浏览器最核心的部分是 “Rendering Engine”,即 “渲染引擎”,不过我们一般习惯将之称为 “浏览器内核”。 它主要包括以下线程:

下面我们来分别介绍渲染进程中的每个线程。

2.1 GUI 渲染线程

GUI 渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。

2.2 JavaScript 引擎线程

JavaScript 引擎线程负责解析 JavaScript 脚本并运行相关代码。 JavaScript 引擎一直等待着任务队列中任务的到来,然后进行处理,一个Tab页(Renderer 进程)中无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。

需要注意的是,GUI 渲染线程与 JavaScript 引擎线程是互斥的,所以如果 JavaScript 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染被阻塞。

2.3 事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 引擎是单线程的,所有这些事件都得排队等待 JavaScript 引擎处理。

2.4 定时触发器线程

浏览器定时计数器并不是由 JavaScript 引擎计数的,这是因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,所以通过单独线程来计时并触发定时是更为合理的方案。我们日常开发中常用的 setInterval 和 setTimeout 就在该线程中。

2.5 Http 异步请求线程

在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

前面我们已经知道了,由于 JavaScript 引擎与 GUI 渲染线程是互斥的,如果 JavaScript 引擎执行了一些计算密集型或高延迟的任务,那么会导致 GUI 渲染线程被阻塞或拖慢。那么如何解决这个问题呢?嘿嘿,当然是使用本文的主角 —— Web Workers。

三、Web Workers 是什么

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。

在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,可以在独立线程中处理一些计算密集型或高延迟的任务,从而允许主线程(通常是 UI 线程)不会因此被阻塞或拖慢。

(图片来源:https://thecodersblog.com/web...

3.1 Web Workers 的限制与能力

通常情况下,你可以在 Worker 线程中运行任意的代码,但注意存在一些例外情况,比如:直接在 worker 线程中操纵 DOM 元素,或使用 window 对象中的某些方法和属性。 大部分 window 对象的方法和属性是可以使用的,包括 WebSockets,以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。

下面我们以 Chrome 和 Opera 所使用的 Blink 渲染引擎为例,介绍该渲染引擎下 Web Worker 中所支持的常用 APIs:

  • Cache:Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。
  • CustomEvent:用于创建自定义事件。
  • Fetch:Fetch API 提供了一个获取资源的接口(包括跨域请求)。任何使用过 XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集。
  • Promise:Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
  • FileReader:FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
  • IndexedDB:IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件/二进制大型对象(blobs)。
  • WebSocket:WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
  • XMLHttpRequest:XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。

更多信息请参见: Functions and classes available to workers

3.2 主线程与 Web Workers 之间的通信

主线程和 Worker 线程相互之间使用 postMessage() 方法来发送信息,并且通过 onmessage 这个事件处理器来接收信息。数据的交互方式为传递副本,而不是直接共享数据。主线程与 Worker 线程的交互方式如下图所示:

(图片来源:https://viblo.asia/p/simple-web-workers-workflow-with-webpack-3P0lPkobZox

除此之外,Worker 还可以通过 XMLHttpRequest 来访问网络,只不过 XMLHttpRequest 对象的 responseXMLchannel 这两个属性的值将总是 null

四、Web Workers 的分类

Web Worker 规范中定义了两类工作线程,分别是专用线程 Dedicated Worker 和共享线程 Shared Worker,其中,Dedicated Worker 只能为一个页面所使用,而 Shared Worker 则可以被多个页面所共享。

4.1 Dedicated Worker

一个专用 Worker 仅仅能被生成它的脚本所使用,其浏览器支持情况如下:

(图片来源:https://caniuse.com/#search=Web%20Workers

需要注意的是,由于 Web Worker 有同源限制,所以在进行本地调试或运行以下示例的时候,需要先启动本地服务器,直接使用 file:// 协议打开页面的时候,会抛出以下异常:

Uncaught DOMException: Failed to construct 'Worker': 
Script at 'file:///**/*.js' cannot be accessed from origin 'null'.
4.1.1 专用线程 Dedicated Worker:Ping/Pong

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Worker —— Ping/Pong</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Worker —— Ping/Pong</h3>
    <script>
      if (window.Worker) {
        let worker = new Worker("dw-ping-pong.js");
        worker.onmessage = (e) =>
          console.log(`Main: Received message - ${e.data}`);
        worker.postMessage("PING");
      } else {
        console.log("呜呜呜,不支持 Web Worker");
      }
    </script>
  </body>
</html>

dw-ping-pong.js

onmessage = (e) => {
  console.log(`Worker: Received message - ${e.data}`);
  postMessage("PONG");
}

以上代码成功运行后,浏览器控制台会输出以下结果:

Worker: Received message - PING
Main: Received message - PONG

每个 Web Worker 都可以创建自己的子 Worker,这允许我们将任务分散到多个线程。创建子 Worker 也很简单,具体我们来看个例子。

4.1.2 专用线程 Dedicated Sub Worker:Ping/Pong

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Sub Worker —— Ping/Pong</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Sub Worker —— Ping/Pong</h3>
    <script>
      if (window.Worker) {
        let worker = new Worker("dw-ping-pong.js");
        worker.onmessage = (e) =>
          console.log(`Main: Received message - ${e.data}`);
        worker.postMessage("PING");
      } else {
        console.log("呜呜呜,不支持 Web Worker");
      }
    </script>
  </body>
</html>

dw-ping-pong.js

onmessage = (e) => {
  console.log(`Worker: Received message - ${e.data}`);
  setTimeout(() => {
    let worker = new Worker("dw-sub-ping-pong.js");
    worker.onmessage = (e) => console.log(`Worker: Received from sub worker - ${e.data}`);
    worker.postMessage("PING");
  }, 1000);
  postMessage("PONG");
};

dw-sub-ping-pong.js

onmessage = (e) => {
  console.log(`Sub Worker: Received message - ${e.data}`);
  postMessage("PONG");
};

以上代码成功运行后,浏览器控制台会输出以下结果:

Worker: Received message - PING
Main: Received message - PONG
Sub Worker: Received message - PING
Received from sub worker - PONG
4.1.3 专用线程 Dedicated Worker:importScripts

其实在 Web Worker 中,我们也可以使用 importScripts 方法将一个或多个脚本同步导入到 Web Worker 的作用域中。同样我们来举个例子。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>专用线程 Dedicated Worker —— importScripts</title>
  </head>
  <body>
    <h3>阿宝哥:专用线程 Dedicated Worker —— importScripts</h3>
    <script>
      let worker = new Worker("worker.js");
      worker.onmessage = (e) => console.log(`Main: Received kebab case message - ${e.data}`);
      worker.postMessage(
        "Hello, My name is semlinker."
      );
    </script>
  </body>
</html>

worker.js

importScripts("https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js");

onmessage = ({ data }) => {
  postMessage(_.kebabCase(data));
};

以上代码成功运行后,浏览器控制台会输出以下结果:

Main: Received kebab case message - hello-my-name-is-semlinker
4.1.4 专用线程 Dedicated Worker:inline-worker

在前面的例子中,我们都是使用外部的 Worker 脚本来创建 Web Worker 对象。其实你也可以通过 Blob URL 或 Data URL 的形式来创建 Web Worker,这类 Worker 也被称为 Inline Worker。

1. 使用 Blob URL 创建 Inline Worker

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>,对应的示例如下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 <img><a> 中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。

const url = URL.createObjectURL(
  new Blob([`postMessage("Dedicated Worker created by Blob")`])
);

let worker = new Worker(url);
worker.onmessage = (e) =>
  console.log(`Main: Received message - ${e.data}`);

除了在代码中使用字符串动态创建 Worker 脚本,也可以把 Worker 脚本使用类型为 javascript/workerscript 标签内嵌在页面中,具体如下所示:

<script id="myWorker" type="javascript/worker">
   self['onmessage'] = function(event) {
     postMessage('Hello, ' + event.data.name + '!');
   };
</script>

接着就是通过 script 对象的 textContent 属性来获取对应的内容,然后使用 Blob API 和 createObjectURL API 来最终创建 Web Worker:

<script>
  let workerScript = document.querySelector('#myWorker').textContent;
  let blob = new Blob(workerScript, {type: "text/javascript"});
  let worker = new Worker(URL.createObjectURL(blob));
</script>

2. 使用 Data URL 创建 Inline Worker

Data URLs 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:

data:[<mediatype>][;base64],<data>

mediatype 是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII。如果数据是文本类型,你可以直接将文本嵌入(根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行 base64 编码之后再进行嵌入。

const url = `data:application/javascript,${encodeURIComponent(
  `postMessage("Dedicated Worker created by Data URL")`
)}`;

let worker = new Worker(url);
worker.onmessage = (e) =>
  console.log(`Main: Received message - ${e.data}`);

4.2 Shared Worker

一个共享 Worker 是一种特殊类型的 Worker,可以被多个浏览上下文访问,比如多个 windows,iframes 和 workers,但这些浏览上下文必须同源。相比 dedicated workers,它们拥有不同的作用域。其浏览器支持情况如下:

(图片来源:https://caniuse.com/#search=Web%20Workers

与常规的 Worker 不同,首先我们需要使用 onconnect 方法等待连接,然后我们获得一个端口,该端口是我们与窗口之间的连接。

4.2.1 共享线程 Shared Worker:点赞计数器

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>共享线程 Shared Worker</title>
  </head>
  <body>
    <h3>阿宝哥:共享线程 Shared Worker</h3>
    <button id="likeBtn">点赞</button>
    <p>阿宝哥一共收获了<span id="likedCount">0</span>个👍</p>
    <script>
      let likes = 0;
      let likeBtn = document.querySelector("#likeBtn");
      let likedCountEl = document.querySelector("#likedCount");

      let worker = new SharedWorker("shared-worker.js");
      worker.port.start();

      likeBtn.addEventListener("click", function () {
        worker.port.postMessage("like");
      });

      worker.port.onmessage = function (val) {
        likedCountEl.innerHTML = val.data;
      };
    </script>
  </body>
</html>

shared-worker.js

let a = 666;

console.log("shared-worker");
onconnect = function (e) {
  var port = e.ports[0];

  port.onmessage = function () {
    port.postMessage(a++);
  };
};

在 Shared Worker 的示例页面上有一个 点赞 按钮,每次点击时点赞数会加 1。首先你新开一个窗口,然后点击几次。然后新开另一个窗口继续点击,这时你会发现当前页面显示的点赞数是基于前一个页面的点赞数继续累加。

4.2.2 调试 Shared Workers

在实际项目开发过程中,若需要调试 Shared Workers 中的脚本,可以通过 chrome://inspect 来进行调试,具体步骤如下图所示:

4.3 Service Workers

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。

(图片来源:https://www.pavlompas.com/blo...

Service workers 的浏览器支持情况如下:

由于 Service workers 不是本文的重点,这里阿宝哥就不展开介绍了,感兴趣的小伙伴请自行了解一下。下面我们开始介绍 Web Workers API。

五、Web Workers API

Worker() 构造函数创建一个 Worker 对象,该对象执行指定的URL脚本。这个脚本必须遵守同源策略 。如果违反同源策略,则会抛出一个 SECURITY_ERR 类型的 DOMException。

5.1 Worker 构造函数

Worker 构造函数的语法为:

const myWorker = new Worker(aURL, options);

相关的参数说明如下:

  • aURL:是一个 DOMString 表示 worker 将执行的脚本的 URL。它必须遵守同源策略。
  • options(可选):包含可在创建对象实例时设置的选项属性的对象。可用属性如下:

    • type:用以指定 Worker 类型的 DOMString 值. 该值可以是 classic 或 module。如果未指定,将使用默认值 classic。
    • credentials:用以指定 worker 凭证的 DOMString 值。该值可以是 omit,same-origin 或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证)。
    • name:在 DedicatedWorkerGlobalScope 的情况下,用来表示 Worker 的 scope 的一个 DOMString 值,主要用于调试目的。

需要注意的是,在创建 Web Worker 的时候,可能会出现以下异常:

  • 当 document 不被允许启动 worker 的时候,将抛出一个 SecurityError 异常。比如:如果提供的 aURL 有语法错误,或者与同源策略相冲突(跨域访问)。
  • 如果 worker 的 MIME 类型不正确,将抛出一个 NetworkError 异常。worker 的 MIME 类型必须是 text/javascript
  • 如果 aURL 无法被解析(格式错误),将抛出一个 SyntaxError 异常。

示例

const worker = new Worker("task.js");

当我们调用 Worker 构造函数后会返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下:

  • Worker.onerror:指定 error 事件的监听函数。
  • Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在 Event.data 属性中。
  • Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • Worker.postMessage():向 Worker 线程发送消息。
  • Worker.terminate():立即终止 Worker 线程。

5.2 Dedicated Worker 示例

下面我们再来举一个 Dedicated Worker 的例子:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dedicated Worker Demo</title>
  </head>
  <body>
    <h3>Dedicated Worker Demo</h3>
    <script>
      const worker = new Worker("task.js");

      worker.postMessage({
        id: 666,
        msg: "Hello Semlinker",
      });

      worker.onmessage = function (message) {
        let data = message.data;
        console.log(`Main: Message from worker ${JSON.stringify(data)}`);
        worker.terminate();
      };

      worker.onerror = function (error) {
        console.log(error.filename, error.lineno, error.message);
      };
    </script>
  </body>
</html>

task.js

而 Dedicated Worker 所执行的代码如下所示:

onmessage = function (message) {
  let data = message.data;
  console.log(`Worker: Message from main thread ${JSON.stringify(data)}`);
  data.msg = "Hi from task.js";
  postMessage(data);
};

以上代码成功运行后,控制台会输出以下结果:

Worker: Message from main thread {"id": 666,"msg": "Hello Semlinker"}
worker-demo.html:20 Main: Message from worker {"id":666, "msg":"Hi from task.js"}

为了让大家更好的理解 Web Worker 的工作流程,我们来了解一下 WebKit 加载并执行 Worker 线程的流程:

(图片来源:http://www.alloyteam.com/2015...

5.3 观察 Dedicated Worker

看到这里相信有些小伙伴会好奇,介绍了那么多 Web Worker 的相关知识,在哪里可以直观地感受到 Web Worker,接下来我们将从以下两个角度来观察它。

5.3.1 开发者工具

这里阿宝哥以 Chrome 浏览器为例,首先打开 Chrome 开发者工具,然后选择 Sources -> Page

5.3.2 Chrome 任务管理器 & 活动监视器

打开 Chrome 任务管理器之后,我们可以找到当前 Tab 页对应的进程 ID,即为 5194,接着我们打开 macOS 下的活动监视器,然后选中 5194 进程,然后对该进程进行取样操作:

取样完成后,可以看到当前渲染进程中完整的线程信息,红框中标出的就是我们想要找的 Dedicated Worker

本来是想一口气写完 “你不知道的 Web Workers”,但考虑到部分小伙伴们的感受,避免出现以下群友提到的情况,阿宝哥决定拆成上下两篇。

下篇阿宝哥将着重介绍 Web Worker 一些常见的使用场景和 Deno Web Workers 的相关实现,感兴趣的小伙们记得持续关注阿宝哥哟。

六、参考资源

七、推荐阅读

查看原文

zzard 赞了文章 · 2020-06-05

图解JavaScript——进阶篇(执行上下文、变量对象、作用域、作用域链、闭包、this、原型及原型链、事件循环等一把梭)

关注公众号“执鸢者”,回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频,回复“js02”获取本节整体思维导图。
使用思维导图阐述JS的知识,为面试及工作助力。本节是第二部分,主要内容包括函数的执行(函数执行过程、执行上下文、变量对象、作用域、作用域链、闭包、this指向、原型及原型链等)和事件循环(任务分类、Event Loop等)

一、函数执行过程

本节主要从代码整个执行过程出发,然后阐述遇到函数后如何执行,紧接着分点对执行过程中的关键知识点(执行上下文、变量对象、作用域、作用域链、this指向、闭包、原型、原型链等)进行阐述,按照自己的理解将整个过程进行了叙述。

1.1 JavaScript代码的整体执行过程

1.2 函数执行过程

1.3 执行上下文

1.4 变量对象

1.5 作用域

1.6 JavaScript查询方式

1.7 作用域链

1.8 闭包

1.9 this

1.10 new

1.11 原型/构造函数/实例

1.12 原型链

二、事件循环

对事件循环的任务进行了简要分类,然后对整个事件循环过程进行了叙述。

相关章节<br/>
图解JavaScript————基础篇<br/>
欢迎大家关注公众号(回复“js02”获取本节的思维导图,回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
查看原文

赞 5 收藏 4 评论 0

zzard 赞了文章 · 2020-06-05

第二期:前端九条bug分享

本期

这次9个并不都是bug, 其中有几个小优化, 虽然一个月的时间遇到很多bug, 但并不是每个都有参考价值, 让我们看看这次我遇到的有趣问题吧.

1: git 报错WARNING: POSSIBLE DNS SPOOFING DETECTED!

bug现象:
一个平凡的清晨, 放下书包喝了口白开水, 习惯性的git pull了一下, 这个不速之客就出现在了命令行里面, 脑袋中竟然第一反应是希望这个bug有趣一点, 这样就可以写文章了, 这个bug是突然出现的本身肯定与我的操作无关, 当时问了下其他同事也遇到了这个问题, 但是我需要查一下如何解决它,以及它的危害与预防.

bug追查:
WARNING: POSSIBLE DNS SPOOFING DETECTED! 翻译成中文 警告:检测到可能的DNS欺骗!哦哦原来是"信任"方面的问题, 那我能想到的就是"身份验证方面", 有关身份我只做过全局的gitlab的账号邮箱设置, 权限设置, 还有就是ssh的设置, 那么怎么看都是ssh嫌疑最大, 最后还是去网上查到的解决方法.

bug解决方案:
删除 known_hosts 文件
一台主机上有多个Linux系统,会经常切换,那么这些系统使用同一ip,登录过一次后就会把ssh信息记录在本地的~/.ssh/known_hsots文件中,
切换该系统后再用ssh访问这台主机就会出现冲突警告,需要手动删除修改known_hsots里面的内容。
有以下两个解决方案:

  1. 手动删除修改known_hsots里面的内容;
  2. 修改配置文件“~/.ssh/config”,加上这两行,重启服务器。
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

优缺点:

  1. 需要每次手动删除文件内容,一些自动化脚本的无法运行(在SSH登陆时失败),但是安全性高;
  2. SSH登陆时会忽略known_hsots的访问,但是安全性低;

2:地图geojson 绘制地图的具体操作(附上地图的代码)

需求:
项目内需要使用地图展示资产在全球的分布, 需要绘制'全球地图',有闪烁提示有悬停展示详情, 本功能只是一个小模块, 所以不许有明显的加载感, 后端会返回给我每个点对应的经纬度.

分析与准备:

本次并没有采用我们公司内部孵化的地图库, 因为它并不够轻量, 最后选用的是echarts, 我也是第一次用echarts画地图.
说实话官网的例子挺不好的, 很多地方也没有说明白反正我第一次去学习的体验挺差.
地图需要画出各个国家的区域线条, 那就需要一个描述文件而这个文件一般都叫做"geojson"文件, 而这个文件又分为pc(以中心点绘制)、以经纬度绘制两种比较常见, 而本次我们采用的是经纬度的定位, 那就需要用经纬度的geojson!! 这个非常重要, 具体的json这个网站里面挺全的, 但他都是pc绘制geojson文件库
下面是我简单做了下封装的组件
<template>
<div
  id="main"
  style="position: relative; width: 100%; height: 100%; padding: 0px; margin: 0px; border-width: 0px; cursor: default;"
/>
</template>

<script>
import echarts from 'echarts';
import geoJson from '../assets/geojson/countries.geo.json';

export default {
props: {
  mapData: Array, // 里面只有三个值, 经度, 纬度, 数量 用于打点
},
data() {
  return {
    sanData: [],
    myChart: null,
    tooltip: { // 提示框
      trigger: 'item',
      formatter: (arg) => {
        if (arg.value) {
          return `${arg.name}: ${arg.value[2]}`;
        }
        return '';
      },
    },
    geo: { // 地图的底色样式
      map: 'all',
      zoom: 1,
      top: 30,
      left: 30,
      right: 30,
      bottom: 30,
      show: true,
      roam: false,
      label: {
        normal: {
          show: false,
        },
        emphasis: {
          show: false,
        },
      },
      itemStyle: {
        normal: {
          areaColor: '#47D1FF', // 板块颜色
          borderColor: '#3B5077', // 边线
          shadowColor: 'rgba(0, 0, 0, 0.2)',
          shadowBlur: 10,
        },
        emphasis: {
          areaColor: '#2B91B7', // 悬停
        },
      },
    },
    effect: { // 闪点样式
      name: '数量',
      type: 'effectScatter',
      showEffectOn: 'render', // render emphasis
      coordinateSystem: 'geo',
      hoverAnimation: true,
      legendHoverLink: true,
      symbolSize: () => 20,
      rippleEffect: {
        brushType: 'stroke',
      },
      itemStyle: {
        normal: {
          color: '#ff8003',
        },
      },
      label: {
        normal: {
          formatter(arg) {
            return arg.value[2];
          },
          position: 'inside',
          show: true,
        },
        emphasis: {
          show: false,
        },
      },
    },
    map: { // 地图的绘制数据
      type: 'map',
      name: '工程数',
      mapType: 'world', // 自定义扩展图表类型
      geoIndex: 0,
      itemStyle: {
        normal: { label: { show: true } },
        emphasis: { label: { show: true } },
      },
    },
  };
},
methods: {
  initMap() {
    this.myChart = echarts.init(document.getElementById('main'));
    const {
      myChart, sanData, effect, tooltip, geo, map,
    } = this;
    echarts.registerMap('all', geoJson, {});
    effect.data = sanData;
    const option = {
      geo,
      tooltip,
      series: [effect, map],
    };
    myChart.setOption(option);
  },
  // 组装成地图需要的打点数据
  initDian() {
    this.mapData.forEach((item) => {
      this.sanData.push({
        name: item.country,
        value: [
          item.lng,
          item.lat,
          item.value,
        ],
      });
    });
  },
},
mounted() {
  this.initDian();
  this.initMap();
},
};
</script>
效果如下图(参考了网上大家的做法, 比官网还靠谱)

sj.png

3:样式一样的新老项目共用一个域名.

背景:
一个辗转了三个团队, 维护了快三年的老vue项目, 代码简直乱到令人发指的程度, 比如请求报错了 /api/home/list/ip这样的地址报401, 那么我去查找这个地址在哪里发出的请求, 全局也没搜索到这个地址, 一个小时后我才知道 这个地址被分成了类似这种-> , const a = 'api', const b = 'home', const c = 'list', 请求的时候 axios.get(/${a}/${b}/${c}/ip), 不仅如此, 这个请求还被放在的vuex里面辗转反侧三个配置页面然后被各种重新命名之后不知所踪, 关键是连个文档也没有, 这个项目在我来到公司之前就已经是一个人人不愿碰的存在了.

我刚加入就接到了改造这个项目的任务
经过讨论这个项目是"救不活了", 但是我们可以另起一个一模一样的项目, 然后所有的新需求都放在新项目里面, 每次需求都把老项目的页面迁移过来一个, 这样一年半载之后就可以彻底使用新项目代替老项目

需要解决的问题:

  1. 两个项目域名相同
  2. 切换时尽量做到用户无感
  3. 共用一套登录系统
  4. 平稳迁移, 最终淘汰老项目

解决方案:

  1. 老项目地址xxx/home, 新项目地址xxx/new, 这个需要后端同学配置nginx代理, 监测到xxx后面的地址进行分配, 这样两个工程的静态资源也要做好代理的区分, 我们需要配置好publicPath.
  2. 俩个工程的一级二级导航栏做到完全一样, 甚至悬停出现的弹框也要做到完全一致(视觉骗子), 还好这个项目导航方面并不是很复杂.
  3. 添加一个专门的router拦截函数, 老项目: 监测到new字段就跳转到新项目, 新项目检测到home路径就跳到老项目, 这样写对于404有bug最后我说解决的办法.
  4. 后台代码也跟我们一期一起重写, 这就要兼容新老后端代码.

404问题:
如果监测到new字段就跳转到新项目, 检测到home路径就跳到老项目, 这个会有bug, 比如我乱打一个xxx/jjj那么这个就会留在原地, 我们就需要在'新老'两个项目里面都写一个'404页面', 这一点想到了那么如果是这样 xxx/new/jjj这又是一个'404页面'但是他带new字段, 那么它跳到新项目又变成了bug, 我想到的解决方案是 new这个新项目只要检测到不是自己路由表里的地址, 就把这个连接指向老项目, 老乡维护一套新项目的路由地址list, 只有在list内才会进行跳转新项目的操作, 这样bug就解决了, 可能你会有更好的方法哦可以一起交流.

401与303 前端 测试 后端:

这里是个交流沟通的问题
问题是这样的, 老项目接口未登录报303, 新项目接口未登录报401, 这个现象看起来很好办, 只要检测到 303或是401统一跳登录页面, 但是问题来了, 这两个状态码的判断标准不一样, 出现了'循环跳转'的bug.

比如说: 你在老项目里面不报303那么登录页面就把你转到'来时的'路径, 但是这个路径如果指向新项目, 但是新型项目里面报了401, 导致新项目又跳回登录页面, 日复一年年复一年的循环跳转.

这种问题是沟通问题

只要出现循环的跳转, 测试就会来找到我们, 因为这看起来绝壁是前端问题啊, 你死循环管后端什么事, bug提给我不管是不是我的问题, 有时间的话我都会帮着排查与修复, 我把这个原因搞清楚后去跟后端同学沟通, 这个过程挺慢的, 毕竟谁也不希望bug是自己的..., 可是这个问题到第一次上线他们也没有去解决, 线上又报循环跳转测试仍让我紧急修复, 这个虽然能够理解但也挺无语, 后来用的技巧就是把问题在群里抛出来, 用简短的话把问题说清楚, 不能"看着像前端问题就前端改", 当然了我跟后台同学了解下303余与401的判断标准, 在前端侧也增加了一层登录判断, 虽然没啥大用但是也能帮后端同学解决95%的差异了, 可是剩下5%的问题我已经帮他们分析出解决方案, 可是也放在下个版本做了.

通过这些问题能够感受到, 前端不光是写代码, 也有统筹整个项目的责任在身上, 积极整合大家也是一种锻炼

4:element表单隐藏input

故事是这样的
作者接到了一个离职者的遗留工程的新增需求,需求很简单:在一个表单中根据某一项的选择情况来决定显示与隐藏一个input,比如'类型下拉框'选择‘个人’就隐藏‘订单号’反之则显示并且为'必填', 这个需求简直简单到爆。

bug描述
当先不选择'个人', 然后再选择'个人'选项时,‘订单号的input’消失了但是报错提示信息'订单号为必填'还是存在。

bug初步分析
element-ui本身是否有缺陷, 我用v-if控制输入框的隐藏它没有检测到。

bug初步解决
在我隐藏这个输入框后, 100毫秒的时候调用一次表单的验证方法,虽然你能够解决但这个方法给人的感觉就是临时方案。

bug深入分析
建一个vue工程把这个bug情况在纯净的新工程里面还原一次,结果并未复现, 这就说明这bug跟人家element没有关系,定位在代码本身有问题就好了,问题的元凶就在@change这个事件里面, 上一位同学是在行间这样写的。
@change="() => { updateDesc(); updateYtsUserType(); updateMonitoring() }"
而在updateMonitoring方法里面有一个对表单的校验,这个updateMonitoring方法还有5处地方在调用,所以并不建议修改这里, 导致这一bug的原因是@change事件与vue的数据更新机制未衔接好, vue更新一个值是要经过diff算法的, 更新数据采用三种方案,1:事件的订阅发布 2: promise 3:settimeout 逐级兼容, change事件恰好在这之前就调用了表单的验证,所以把这个检验放在下一个宏任务就不会出现这个bug了, 也不会出现错误信息一闪而逝的情况。

想起了聚焦与失焦的问题(某些移动端有bug)
之前做移动端项目有三个input, 我需要监控当前用户从某个input框里面填完再跳到某个input框的操作路径, 并做一些相应的处理,具体的业务场景我记不清了, 但是当时我发现,比如我再inputa里面输入内容后跳到inputb里面,有时候是先执行的inputb的聚焦 然后 才是inputa的失焦 并且这个时间不是固定的, 因为当时我搞了个0秒延时器并不是100%奏效, 这个点大家可以注意一下。
正常因该是 a聚焦-> a失焦-> b->聚焦 pc端还挺好

5:上传打包文件的小优化, 小而美提升工作体验

这种事情我也认为属于小bug, 毕竟程序员是追求高效的, 而且减少操作步骤可以减少出错的概率。

公司项目组暂时由我们上传到服务器,自动化正在搭建。

之前上传代码
1: 打包
yarn build
2:压缩
把打包好的dist文件压缩为zip格式
3:scp上传dist.zip文件
4:ssh连接服务器
5: 进入指定目录, unzip解压dist

现在传代码
1: 打包
yarn build
2:scp上传代码
scp -r dist/* root@1.1.1.1:/home/xxx/xxxt

反思
如果包并不是特别大的话或者网络很不好, 建议使用第二种, 不以优化小而不为,不以危害小而为之。

6: Cannot assign to read only property 'exports' of object '#<Object>'

这个又是接收了一个离职同学的老项目

bug描述
拉新项目-> 切分支-> yarn 一切正常,打开地址就报这个错.
中文意思就是这个项目里面混合使用了common的规范与es的规范, 需要我统一规范, 就比比如使用了 module.exports 也使用了 import

bug初步解决
使用插件让两者兼容就解决了
npm install babel-plugin-transform-es2015-modules-commonjs

babelrc配置
{ "plugins": ["transform-es2015-modules-commonjs"] }

思考
这个问题应该不会是个需要下载插件或者修改代码才能解决的问题, 毕竟开发了很久, 如果启动都有bug那怎么上线的, 所以问题应该就存在于流程上, 或者外部的环境上, 比如它使用了我本地的全局webpack的话那就有可能是我webpack版本问题, 但查了一下package.json文件并无这种情况,那问题就在操作流程上。

bug解决
原因很简单, 这个是个老工程使用npm管理版本, 而我习惯性的用了yarn, 导致yarn命令并不能读取package-lock.json文件,那么我下载的版本与之前同学使用的版本可能出现了不同, 删掉node_modules文件夹npm i, 解决问题

反思:npm与yarn的锁文件可否用插件兼容
npm: package-lock.json
yarn:yarn.lock
可否做一个小插件把这两种文件类型与数据相互的转换, 我分别看了下这两个文件的格式如下
npm
yarn

文件类型各不相同, 还有一个问题就是这两个工具的源也有差异, 可能会出现某个插件某个版本yarn可以安装, npm里面没有等等问题, 这就导致类似的兼容插件没有大的意义了。
总结
大家还是暂时老老实实使用过同一种包管理工具把。

7: 厉害的hover

hover思维的局限
刚入门的时候一个老师给我讲,标签的:hover只能修改这个元素本身与这个元素内部的元素, 所以当时写一个弹出菜单的话需要把这个'标题''弹出框'写在一个div里面, hover这个div的时候弹出框block, 但是其实hover可以选择兄弟元素, 可以选择远方的兄弟元素, 可以给兄弟元素增加条件
举例子

相隔的一个兄弟元素内部的span
 <div class="home">
    <div class="main">被hover的元素</div>
    <div class="content">相邻的元素</div>
    <div class="content">
      <span>远处的元素</span>
      <p>我不变色</p>
    </div>
  </div>
    .main:hover +.content +.content >span {
      color: red;
    }

8:after变色变文案

需求描述
比如我现在有6种数据类型, 每种类型前面要有一个‘彩色的点’来快速表明他是什么类型如下图:
g.png

要解决的问题

  1. 这种修饰性的结构一般第一想法都是用after和before做。
  2. 圆内的文字需要动态传入
  3. 颜色能动态传入就完美了
  4. 因为这个数据可能上百条,所以不用真实dom很有必要。
  5. 不可以用js来修改, 因为不想把简单问题弄得复杂,导致代码工程化被破坏。
  6. 模块化,最好其他地方加上一个class名就有了这个效果。

解决问题

  1. 这个要做成模块,中间的文字就要可配,我想到了attr()函数。
  2. 颜色实在没想到动态的传入方式, 只能退而求其次用scss老哥的@each助阵一下了, 还好最多只有6种配色。
  3. 一个class还不够, 需要data-*属性来配合。

上才艺,咳咳不是,是上代码

<div data-color="red"  data-title="1">动漫</div>
    <div data-color="green" data-title="2">小说</div>
    <div data-color="blue" data-title="3">狗狗</div>
*[data-color]::after {
  content: attr(data-title);
  color: white;
  align-items: center;
  display: inline-flex;
  justify-content: center;
  width: 20px;
  height: 20px;
  font-size: 12px;
  margin-left: 10px;
  border-radius: 50%;
}

@each $color in red, green, yellow, blue {
  *[data-color="#{$color}"]::after {
    background-color: $color;
  }
}

注意
attr只能在content中生效。
如果attr不是只能在content里生效就好了,我们就可以完美解决这一问题,这方面现在也在提案中, 但是暂时没有浏览器支持。

受限于css的本性, 这里并不完美, 颜色方面还是要传,如果大家有好的方法可以私信讨论一下。

9: 复制也很有意思

问题描述
我们前端工程师无时无刻都在与json打交道(或是一个大对象),比如我们生成了一个很长的json或者是ajax获取到了一段很长的json, 我们想要把这个json拿出来单独作为一个文件本地引入, 或者是在控制台看着累要拿到某些工具中进行解析,为了提升性能会出现很多..., 除此之外还会出现对象类型需要手动打开等情况, 说实话复制出来不是太方便

起因是一个晨会的知识分享

用鼠标复制还是处于网民阶段, 成需要肯定是利用方法

同学a:

使用window.copy方法把数据复制到剪切板里面就可以拿到了, 对于开发环节来说又不用在乎兼容性, 这个方法我试了一下也并没有长度限制挺好用的。

同学b:

使用js直接把json数据生成在一个文件里面岂不是更方便, 听到这个的时候就是感觉js不是不可以操纵用户的文件系统么。。。 有点打破我的固有认知, 但是试了一下他提供的方法, 还真不错,下面我就介绍一下这个方法与原理:上才艺

(function(console){

  console.save = function(data, filename){

  if(!data) {

  console.error('Console.save: No data')

  return;

  }

  if(!filename) filename = 'console.json'

  if(typeof data === 'object'){

  data = JSON.stringify(data, undefined, 4)

  }

  var blob = new Blob([data], {type: 'text/json'}),

  e = document.createEvent('MouseEvents'),

  a = document.createElement('a')

  a.download = filename

  a.href = window.URL.createObjectURL(blob)

  a.dataset.downloadurl = ['text/json', a.download, a.href].join(':')

  e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)

  a.dispatchEvent(e)

  }

})(console)

console.save({a:1})

// 这里是解释版, 每个语句的意思都详细的解释

(function(console){
  console.save = function(data, filename){
  if(!data) { // 1:没东西没有意义, 大部分可能是复制错了
      console.error('Console.save: No data')
     return;
  }
  // 2:名字肯定要有个
  if(!filename) filename = 'console.json'

  if(typeof data === 'object'){ // 3:转成字符串
    // 4: JSON.stringify
    // 第一个参数:就是数据了
    // 第二个参数:指定哪些key需要处理, undefined就是都处理
    // 第三个参数: 层级之间的空格缩进数
     data = JSON.stringify(data, undefined, 4)
  }
  // 这个构造函数厉害了: Blob
  // Blob类型的对象表示不可变的类似文件对象的原始数据。
  // Blob对象是二进制数据,但它是类似文件对象的二进制数据,因此可以像操作File对象一样操作Blob对象
  // Blob 表示的不一定是JavaScript原生格式的数据(这句最关键)。File 接口基于Blob
  // 第一个参数是数组, 就是个拼接,这个自己拼也行
  // 第二个参数是配置, 这里我们指定为json文件
  var blob = new Blob([data,data], {type: 'text/json'}),

  // 自定义一个事件
  e = document.createEvent('MouseEvents'),
  // a标签没什么好说的
  a = document.createElement('a')
  // 必须定义, 否则变成了跳转
  a.download = filename
  // URL就是定义本地路径的api, 我们做一个上传组件 或是裁剪组件的时候, 会用这种方式展示本地的图片
  a.href = window.URL.createObjectURL(blob)
  // dataset就是获取属性的意思
  // 格式->> text/json:文件名:blob数据
  a.dataset.downloadurl = ['text/json', a.download, a.href].join(':')
  // 我们的自定义事件配置
  e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
  // 触发我们的自定义事件
  a.dispatchEvent(e)

  }
})(console)

console.save({a:21},'来段静态文件')

虽然用处真的不大, 但是开阔了思路对未来的变成之路也是有好处的。

end

本次的分享就是这样,我深刻的感受到‘好的解决方法’很多, 最缺少的是好的问题,欢迎交流, 祝每天进步

查看原文

赞 16 收藏 10 评论 3

zzard 赞了文章 · 2020-06-05

JavaScript必须掌握的基础 ---> 原型&原型链

原型和原型链的主要作用:

  • 实现属性和方法的公用
  • 继承

所以下面的例子全是以构造函数为例。

原型

函数是也是对象,是一个属性的集合,所以函数下也有属性,也可以自定义属性。当我们创建一个函数时就默认会有一个prototype属性,这个属性是一个对象(属性的集合)。这个东西就是原型---通过调用构造函数而创建的那个对象实例的原型对象。prototype里也有个属性constructor,指向的是函数本身。

prototype
function Person() {
}
Person.prototype.name='erdong';
var p1=new Person();
var p2=new Person();
console.log(p1.name); // erdong
console.log(p2.name); // erdong

函数Person有个prototype属性,给这个属性添加一个name的属性。p1 和 p2 为这个函数的实例,当访问 p1.name 和 p2.name 时其值都是 prototype下面 name的值。这个prototype对象就是 p1 和 p2的实例原型,它下面的所以属性和方法 p1 和 p2 都可以获取并使用。

看一下原型对象与构造函数的关系:

那么实例是怎样与原型对象做关联的呢?

__proto__

每个JavaScript对象都具有的一个属性 -- __proto__ 这个属性指向该对象的原型。不过它是一个隐式属性,并不是所有浏览器都支持它,我们可以把它看做一种实例与实例原型之间的联系桥梁。

function Person() {
}
var p1 = new Person();

console.log(p1.__proto__==Person.prototype); // true

上述p1.__proto__与原型对象时相等的,由此可见p1.__proto__指向的是原型对象。

constructor

每个函数都有一个prototype属性,而prototype下都有一个constructor属性,它指向prototype所在函数。

function Person() {
}
console.log(Person.prototype.constructor==Person) // true

以上就是关于原型几个重要的"属性"已经说完了,下面来讲讲原型连。

实例与原型

JavaScript规定,当读取对象的某个属性或方法时,先从自身查找,如果找不到就去其__proto__指向的原型对象上去找,如果找不到就去原型对象的原型对象上查找,如果再找不到就去原型对象的原型对象上去找... , 就这样直到找到最上层,至于哪里是最上层,下面会提到。

function Person() {
}

var p1 = new Person();

console.log(p1.name);// undefined

p1.show();// Uncaught TypeError: p1.show is not a function

上述例子,p1 为构造函数 Person的实例,当访问 p1 的 name 属性和 show 方法时,因为 p1 是刚 new 出来的实例,所以并没有找到。看下面的例子:

function Person() {
}

Person.prototype.name='erdong';

Person.prototype.show=function () {
    console.log(this.name);
}

var p1=new Person();

console.log(p1.name);// erdong

p1.show();// erdong

当在 prototype上添加 name 属性和 show方法后,p1 就可以正确的访问,这就说明 p1 在查找属性(方法)时,在自身没有找到 就会去__proto__所指的原型对象上去查找。再来看一个例子:

function Person() {
}

Person.prototype.name='erdong';

Person.prototype.show=function () {
    console.log(this.name);
}

var p1=new Person();

p1.name = 'chen'

console.log(p1.name);// chen

当我们在 p1(对象) 上添加一个属性 name 这个时候再去访问 p1.name 那么输出的就是 "chen" 而不是 "erdong"。 这就是一个对象查找属性(方法)时的一个规则。

原型的顶层

我们在上述例子查找 p1的name 时当查找到 Person.prototype 还未找到时,我们应该还往下查找,下一级是谁呢? 因为 Person.prototype是对象,那么他就有一个__proto__属性,指向的是其原型对象--也就是其对应构造函数的 prototype。那么Person.prototype 是谁呢?是Object,因为对象可以通过 new Object()创建:

var obj = new Object();
// 我们平时都是通过字面量的形式来写:var obj = { }; 其实就相当于 new Object(); 只不过是javascript在内部执行了。
obj.name = 'erdong';
console.log(obj.name); // erdong

看图:

为什么当查找到 Object.prototype 找不到就输出 undefined 了呢?
因为当在 Object.prototype 也找不到 name 属性,就会去 Object.prototype 指向的原型对象上查找,我们在上面提到,对象与其原型对象是通过 __proto__做关联的,但是javascript中规定,Object.prototype.__proto__是不存在的 也就是null

console.log(Object.prototype.__proto__ === null); // true

这一点要牢记。

原型链

原型链也是JavaScript中很重要的一个概念,之所以说是一个概念,是因为它是不存在的,不像一个对象的属性,或者是一个对象的方法一样实例存在。
我的理解就是--一个(实例)对象的属性或者方法的查找规则。这个规则可以很简单,也可以很复杂。

我们把上面所有的知识总结一下:
每个函数都有一个原型对象(prototype),原型对象又包含一个属性(constructor),指向的是函数本身,函数的实例都有一个隐式原型(__proto__),指向的是构造函数的原型对象(prototype)。

查找规则:当我们访问实例的一个属性时,先从实例自身查找,如果找不到就去其内部指向的原型对象上去查找,如果再找不到,就去其内部指向的原型对象内部指向的原型对象上去查找,就这样一直找到原型的最顶端。

看图:

蓝色的线就表示一条原型链。

改变prototype

function Person() {
}
Person.prototype={
    name: 'erdong',
    sex: '男',
    doSoming: function() {
        console.log(this.name);
    }
}

var p1=new Person();
p1.doSoming();
console.log(p1.__proto__==Person.prototype); // true
console.log(Person.prototype.constructor==Person);// false

上面的例子,将构造函数的prototype属性重写了。虽然p1也能找的到name,但是prototype下的constructor属性不再指向Person了。实际上指向了Object

console.log(Person.prototype.constructor==Object); //true

是因为我们重写了Personprototype,此时Person.prototype只是一个普通的对象。即:

Person.prototype.constructor = Person.prototype.__proto__.constructor = Object.prototype.constructor = Object

constructor属性很重要时,我们可以这样做:

function Person() {
}
Person.prototype={
    constructor: Person,  // 主动加上constructor属性
    name: 'erdong',
    sex: '男',
    doSoming: function() {
        console.log(this.name);
    }
}
console.log(Person.prototype.constructor==Person);// true

上述代码实例可以适用于当构造函数拥有很多方法或者属性时的写法。

继承

--

原型链是实现继承的一种方式。这里只是略提一下,下面的文章会详细理解。

当我们不去完全重写函数的prototype属性,而是让它等于另一个构造函数的实例时结果会怎样呢?

function SuperType() {
}
SuperType.prototype.name = 'erdong';
SuperType.prototype.getName=function() {
    return this.name;
}

function SubType() {
}
SubType.prototype=new SuperType();

var instance=new SubType();

console.log(instance.name); // erdong
console.log(instance.getName()); // erdong

上述例子中有两个构造函数SuperTypeSubTypeSuperType原型上有个name属性和getName 方法。instance是另一个构造函数SubType的实例,原本instanceSuperType是没有关系的。但是现在instance可以获取到 name 属性和 getName 方法。原因就是SubType重写了prototype属性,让它的值等于SuperType的实例。所以存在SuperType.prototype中的属性和方法,现在也存在与SubType.prototype中。

看下面的图:

蓝色的线为SubType.prototype改变后的原型( __proto__ 和prototype)的指向(也是实例查找属性的路线),红色为原来的原型( __proto__和prototype)的指向。

JavaScript高级程序设计一书中解释上述的示例为原型链的基本概念--当我们让原型对象等于另一个构造函数的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个构造函数的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

关于原型的方法

isPrototypeOf、getPrototypeOf、instanceof、in、hasOwnProperty

isPrototypeOf

用于判断传入的对象内部是否有一个原型对象的指针。

function Person(){
}
var p = new Person();
console.log( Person.prototype.isPrototypeOf( p ) );//  true

我们上面讲到实例与原型对象是通过__proto__做关联的,__proto__并不是Javascript规范,所以我们现实中不能使用它来判断实例与原型对象的关系,这个时候就用isPrototypeOf

getPrototypeOf

ES6 Object新增方法,返回的是传入对象的原型。

function Person(){
}
var p = new Person();
console.log( Object.getPrototypeOf(p)===Person.prototype );//  true

上述代码输出的是 true 证明 Object.getPrototypeOf(p) 获取到的就是 p 的原型。

instanceof

判断前者是否是后者的一个实例。

function Person(){
}
var p = new Person();

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true

由于 p 既是 Person的实例,同时它也是一个对象,所以也是Object的实例。

看下面的示例:

console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true

Function既是Object的实例,Object又是Function的实例。是有点绕了,下面会说明这一情况。

in

判断前者是否是后者原型链中的一个属性。

function Person() {
}

Person.prototype.name='erdong';

var p=new Person();
p.sex='男';

console.log('sex' in p); // true
console.log('name' in p); // true
console.log('address' in p); // false
hasOwnProperty

检测传入的字符串是否是调用者的自身属性,如果是自身的属性,返回true,如果是原型中的属性或者不存在,返回false。

function Person() {
}

Person.prototype.name='erdong';

var p=new Person();
p.sex='男';

console.log(p.hasOwnProperty('name'));// false
console.log(p.hasOwnProperty('sex')); // true
console.log(p.hasOwnProperty('address')); // false

与 in 不同的是如果该属性存在于实例上包括原型链上,就返回true,而hasOwnProperty只有是自身的属性,才会返回true。

思考

我们(构造)函数也是对象,上面说过对象下面都会有一个__proto__属性,那么函数的__proto__指向谁呢?

console.log(Person.__proto__===Function.prototype); // true 

函数都是通过 new Function()来创建的,虽然我们平时创建函数并不是通过 new

下面这个函数:

function sum (num1, num2) {
    return num1 + num2;
}

其实在JavaScript内部应该是这样实现的:

var sum = new Function("num1", "num2", "return num1 + num2");

所以Person对应的构造函数应该是Function

那么新的问题又来了?

Function也一个函数,也是一个对象,那么他同样也有__proto__属性,也有prototype属性,它们分别指向什么呢?

console.log(typeof Function); // 'function'

console.log(Function.__proto__ === Function.prototype); // true

看到上面是不是会很奇怪?下面解释一下:

Function是一个函数,它也是通过new Function创建的,所以它是被自身创建的,它的__proto__指向的自身的prototype--也就是Function.prototype

那么Function.prototype.__proto__又指向谁呢?

console.log(Function.prototype.__proto__ === Object.prototype)

很显然,Function.prototype.__proto__是一个对象,所以它指向的是Object.prototype

还有一个问题,Object也是一个构造函数,也是一个对象,那么它应该也有prototype__proto__属性,我们在上面说到 Object.prototype. __proto__ null,那么Object.__proto__指向谁呢?

console.log(Object.__proto__ === Function.prototype); // true

上面Object.__proto__ 又指向了Function.prototype,是因为Object是函数,所以它的原型就是Function.prototype

最后总结一下:

看似关系很复杂,其实一条一条捋清楚就有一种恍然大悟的感觉。

写在最后

如果文中有错误,请务必留言指正,万分感谢。

点个赞哦,让我们共同学习,共同进步。

GitHub

查看原文

赞 26 收藏 20 评论 1

zzard 赞了文章 · 2020-05-29

让我们学会使用 CSS 计数器

作者:Ahmad shaded
译者:前端小智
来源:sitepoint
点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

我看来,CSS计数器在web上还没有得到充分利用,尽管它们的支持非常好(IE8+)!。在本文中,我将解释如何在项目中使用CSS计数器,以及一些用例。

什么是CSS计数器

  计数器是css3提供的一个强大的工具,是一种可以让我们使用CSS给元素自动编号的方法。使用它可以很方便对页面中的任意元素进行计数,实现类似于有序列表的功能。但与有序列表相比,css计数器可以对任意元素计数,同时还可以实现个性化计数。

CSS 计数器的相关属性

CSS 计数器需要三个属性,分别是:

  • counter-reset
  • counter-increment
  • counter()

counter-reset

counter-reset:属性用于定义和初始化一个或多个CSS计数器。它可以使用一个或多个标识符作为值,该值指定计数器的名称。

使用语法:counter-reset:[<标识符><整数>?]+|none|inherit

每个计数器名称后面都可以跟一个可选的<整数>值,该值指定计数器的初始值。

注意:

  1. 关键字noneinheritinitial不能作为计数器名称;关键字noneinherit可以作为计counter-reset属性的值。设置none将取消设置计数器;设置inherit将从元素的父元素处继承counter-reset值。该counter-reset属性的默认值为none
  2. 计数器的初始值不是计数器显示时的第一个数字/值。这意味着如果希望计数器从1开始显示,则需要将counter-reset中的初始值设置为零。0是默认的初始值,所以如果省略它,默认情况下它将重置为零;counter-reset允许使用负值。因此,如果希望计数器从零开始显示,可以将其初始值设置为-1

counter-increment

counter-increment属性用于指定一个或多个CSS计数器的增量值。它将一个或多个标识符作为值,指定要递增的计数器的名称。

使用语法:counter-increment:[<标识符><整数>?]+|none|inherit

每个计数器名称(标识符)后面都可以跟一个可选<整数>值,该值指定对于我们所编号的元素每次出现时,计数器需要递增多少。默认增量为1。允许零和负整数。如果指定了负整数,则计数器被递减。

counter-increment属性必须和counter-reset属性配合使用。

counter() 函数

counter()函数必须和content属性一起使用,用来显示CSS计数器。它以CSS计数器名称作为参数,并作为值传递给content属性,而content属性就会使用:before伪元素将计数器显示为生成的内容。

counters()函数也有两种形式:counters(name,string)counters(name,string,style)

name参数也是要显示的计数器的名称。可以使用counter-reset属性来指定计数器的名称。而counters()函数与counter()函数(单数形式)区别在于:counters()函数可以用于设置嵌套计数器。

嵌套计数器是用于为嵌套元素(如嵌套列表)提供自动编号。如果你要将计数器应用于嵌套列表,则可以对第一级项目进行编号,例如,1,2,3等。第二级列表项目将编号为1.1,1.2,1.3等。第三级项目将是1.1.1,1.1.2,1.1.3,1.2.1,1.2.2,1.2.3等。

string参数用作不同嵌套级别的数字之间的分隔符。例如,在'1.1.2'中,点('.')用于分隔不同的级别编号。如果我们使用该counters()函数将点指定为分隔符,则它可能如下所示:

content:counters(counterName,".");

 如果希望嵌套计数器由另一个字符分隔,例如,如果希望它们显示为“1-1-2”,则可以使用短划线而不是点作为字符串值:

 content:counters(counterName,"-");

counter()函数一样,style参数是用来定义计数器的风格。默认情况下,计数器使用十进制数字格式化。具体关于style参数的设置可以参照counter()函数的style参数。

如何添加计数器

1. 为计数器选择一个名称

假设我们有下面的 HTML:

<div class="content">
    <h2>Section</>
    <p><!-- Description --></p>

    <h2>Section</>
    <p><!-- Description --></p>

    <h2>Section</>
    <p><!-- Description --></p>
</div>

我想给每个title元素添加一个数字。为此,我将在父元素上定义一个计数器。

.content {
    counter-reset: section;
}

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

2.递增计数器

这一步对于计数器的工作非常重要。在<h2>元素上,我将创建一个before伪元素,它将用于显示计数器的值。

h2:before {
    counter-increment: section;
}

3.分配计数器

最后一步是使用counter()函数作为content属性的值。 现在,添加了以下内容:

h2:before {
    counter-increment: section;
    content: counter(section);
}

接着,为before伪元素添加一些样式,让它看起来漂亮点:

clipboard.png

事件源码:https://codepen.io/shadeed/pe...

CSS 计数器用例

动态章节标题

根据前面的示例,我们可以编辑before伪元素,如下所示

h2:before {
  counter-increment: section;
  content: "Section " counter(section);
  display: block;
  max-width: 80px;
  font-size: 14px;
  font-weight: normal;
  background-color: rgba(#0277bd, 0.2);
  border-radius: 20px;
  text-align: center;
  padding: 6px 8px;
  margin-bottom: 0.75rem;
}

clipboard.png

事件源码:https://codepen.io/shadeed/pe...

嵌套计数器

假设我们有以下列表:

<ul class="services">
    <li class="services__item">Design
      <ul>
        <li>Web</li>
        <li>Mobile</li>
        <li>Graphic</li>
      </ul>
    </li>
    <li class="services__item">Web Development</li>
    <li class="services__item">Mobile Development
      <ul>
        <li>iOS</li>
        <li>Android</li>
        <li>Windows Phone</li>
      </ul>
    </li>
</ul>

我们想让一级显示 “1.”,二级对应“1.1” 。为此,我们应该执行以下操作:

/* Defining a counter for the main list items */
.services {
    counter-reset: services;
}

.services__item:before {
    counter-increment: services;
    content: counter(services) ".";
}

/* Defining a counter for the sub lists */
.services__item ul {
    counter-reset: sub-services;
}

.services__item li:before {
    counter-increment: sub-services;
    content: counter(services) "." counter(sub-services);
}

clipboard.png

事例源码:https://codepen.io/shadeed/pe...


原文:https://ishadeorddeed.com/art...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

赞 10 收藏 7 评论 0

认证与成就

  • 获得 35 次点赞
  • 获得 31 枚徽章 获得 4 枚金徽章, 获得 11 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-09-09
个人主页被 731 人浏览