Skip to main content

· 6 min read

js 在执行代码时可分为全局上下文、函数上下文、eval 上下文,代码执行过程:创建全局上下文(caller),自上而下执行全局上下文;遇到函数时,函数上下文(callee)被 push 到执行栈顶端;开始执行函数中的代码,caller 被挂起;函数执行完后,callee 被移除出执行栈,继续执行全局上下文

声明标识符

let

  • 作用域是块级作用域
  • 不存在变量声明提前
  • 不能重复定义
  • 存在暂时性死区

    暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
// 上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp
// 导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

// 以下死区比较隐蔽
function bar(x = y, y = 2) {
return [x, y];
}

bar(); // 报错

const

  • 一般用来声明常量,且声明的常量是不允许改变的
  • 块级作用域
  • 存在暂时性死区

var

  • 作用域是函数作用域
  • 存在变量提升

变量提升

明确两点:

  • 函数声明优于变量声明
  • 函数表达式会将变量提升,但是代码在执行的时候才会被赋值
test();
console.log(test); // 输出下面的函数test(),符合第一点
function test() {
console.log("我是函数");
}
console.log(test);
var test = "我是变量";
console.log(test); // 输出'我是变量',符合第二点
var test = function (params) {
console.log("我是函数表达式");
};
console.log(test);
test();

this

this 的指向,是在调用函数时根据上下文所动态决定的

  • 默认绑定
  • 隐式绑定(根据调用关系决定)
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
},
};
var fn1 = foo.fn;
fn1(); // this 指向window,这就是隐式丢失
foo.fn(); // this 指向foo,这也就是隐式绑定,指向最后调用它的对象
  • 显式绑定(call/apply/bind/new 绑定)

new 绑定的优先级比显式 bind 绑定更高。如果构造函数中显式返回一个值,且返回的是一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this 仍然指向实例

  • 箭头函数
function foo() {
return (a) => {
console.log(this.a);
};
}

const obj1 = {
a: 2,
};

const obj2 = {
a: 3,
};

const bar = foo.call(obj1);
console.log(bar.call(obj2)); // 输出2,箭头函数的绑定无法被修改

-->详解 this

call/apply/bind 的区别和实现

三者作用是相同的,都可以改变 this 的指向,区别在于传参不同。call 传的参数是参数列表,相当于...[参数数组],apply 是参数数组,而 bind 是直接改变这个函数的 this 指向并且返回一个新的函数,而不是直接调用立刻执行。

作用域和作用域链

作用域其实可理解为执行上下文中声明的变量和变量的作用范围。

作用域链: 当访问一个变量时,解释器会首先在当前作用域查找,如果没有找到,就去父作用域找,直到找到该变量或者不在父作用域中,这就是作用域链。

闭包

函数嵌套函数时,内层函数调用外层函数变量,并且在全局下可访问,这就是闭包

栈溢出

  • 递归

面试题:有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行 3 次,再次执行无效,如何实现

function sayHi() {
console.log("hi");
}

function threeTimes(fn) {
let times = 0;
return () => {
if (times++ < 3) {
fn();
}
};
}

const newFn = threeTimes(sayHi);
newFn();
newFn();
newFn();
newFn();
newFn(); // 后面两次执行都无任何反应

面试题

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
// serTimeout是异步操作,等待外部循环结束后再执行,此时i=6,于是输出一堆66666
// 解决办法1:立即执行函数
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, 1000);
})(i);
}
// 解决办法2:使用let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
// 每个 let 会和代码块结合起来形成块级作用域

· 7 min read

JavaScript 是一种单线程语言,主要是防止 dom 操作的不确定性。但是在同步执行任务的过程容易受到一些耗时任务的阻塞,比如网络请求、定时器和事件监听等,这样会影响到整体页面的加载;因此需要引入异步编程的能力

回调函数

回调地狱:如果某个业务,依赖于上层业务的数据,上层业务又依赖于更上一层的数据,我们还采用回调的方式来处理异步的话,就会出现回调地狱。

function f2(func) {
console.log("f2");
setTimeout(function () {
func();
}, 500);
}
function func() {
console.log("func");
}

promise

Promise 的方式虽然解决了 callback hell,但是这种方式充满了 Promise 的 then() 方法,如果处理流程复杂的话,整段代码将充满 then

function demo() {
new Promise(function (resolve, reject) {
fetch(url)
.then((data) => {
resolve(data);
})
.catch();
});
}
demo()
.then((data) => {})
.catch();

其实 promise 本质上使用了观察者模式,主要流程是 then 函数收集回调函数-->同步/异步触发 resolve 函数-->resolve 执行回调

先看下 promise 的几种基础特性:

  • promise 主要有三种状态,状态间的转移是不可逆的,只能有 pengding to resolved 或者 pending to rejected 两种变化
    • pending
    • resolved
    • rejected
  • then 函数接收两个参数,分别是成功回调和失败回调
  • then 会返回一个 promise,并且会返回上一次处理 resolve 的结果,即链式调用
  • 捕获错误

基于这三个特性,尝试设计一个 promise

实现状态和 then 函数

function MiniPromise(fn) {
this.status = "pending"; // 状态
this.data = null; // 成功信息
this.err = null; // 错误信息
this.onFulfillCallbacks = []; // 成功回调
this.onRejectCallbacks = []; // 成功回调

let self = this;

function resolve(data) {
// 确保状态单向转移
if (self.status === "pending") {
self.status = "resolved";
self.data = data;
}
// 执行回调
self.onFulfillCallbacks.forEach((onFulfillCallback) => {
onFulfillCallback();
});
}
function reject(err) {
if (self.status === "pending") {
self.status = "rejected";
self.err = err;
}
self.onRejectCallbacks.forEach((onRejectCallback) => {
onRejectCallback();
});
}

try {
fn(resolve, reject);
} catch (e) {
console.log(e);
}
}

MiniPromise.prototype.then = function (onFulfill, onReject) {
let self = this;
if (self.status === "pending") {
// 将执行回调存入回调数组
self.onFulfillCallbacks.push(function () {
onFulfill(self.data);
});
self.onRejectCallbacks.push(function () {
onReject(self.err);
});
}
if (self.status === "resolved") {
onFulfill(self.data);
}
if (self.status === "rejected") {
onReject(self.err);
}
};

let miniPromise = new MiniPromise(function (resolve, reject) {
setTimeout(() => {
resolve("success");
}, 1000);
});
miniPromise.then((data) => {
console.log(data);
});
// 1s后输出'success'

实现链式调用

then 函数应该要先判断传入的参数,如果是值类型直接返回该值,如果是函数或者对象,则返回一个新的 promise。下面稍微改造下 then 函数

MiniPromise.prototype.then = function (onFulfill, onReject) {
let self = this;
let promise = new MiniPromise(function (resolve, reject) {
if (self.status === "pending") {
// 将执行回调存入回调数组
self.onFulfillCallbacks.push(function () {
try {
let value = onFulfill(self.data);
// 将value作为下一个then函数的参数
resolve(value);
} catch (err) {
reject(err);
}
});
self.onRejectCallbacks.push(function () {
try {
let err = onReject(self.err);
reject(err);
} catch (err) {
reject(err);
}
});
}
if (self.status === "resolved") {
try {
let value = onFulfill(self.data);
resolve(value);
} catch (err) {
reject(err);
}
}
if (self.status === "rejected") {
try {
let err = onReject(self.err);
reject(err);
} catch (err) {
reject(err);
}
}
});
return promise;
};
// 正常链式调用
miniPromise
.then((data) => {
console.log(data);
return `1_data`;
})
.then((data) => {
console.log(data);
});
// 输出 success
// 输出 1_success

// 捕获第一次执行then的错误
miniPromise
.then((data) => {
console.log(data);
a = b;
return `1_${data}`;
})
.then(
(data) => {
console.log(data);
},
(err) => {
console.log(err.message);
}
);

Generator

Generator 的问题在于,函数的执行需要依靠执行器,每次都需要通过 g.next() 的方式去执行

function* getNum() {
yield 1;
yield 2;
return 3;
}
const gen = getNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

Generator 是如何实现的呢?比如上面这段代码,通过 babel 转移后的代码如下:

function getNum() {
return regeneratorRuntime.wrap(function getNum$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return 1;

case 2:
_context.next = 4;
return 2;

case 4:
return _context.abrupt("return", 3);

case 5:
case "end":
return _context.stop();
}
}
}, _marked);
}

var gen = getNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

其实 Generator 实现的核心就在于上下文的保存,每一次执行到 yield,其实都执行了一遍传入的生成器函数(getNum$),只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行

async 和 await

  • 更加语义化
  • 内置执行器
  • 返回 promise
  • await 会等待这个 Promise (也可以是任意表达式)完成,并将其 resolve 的结果返回
async function getUser() {
const user = await getUser();
return user;
}
getUser().then((res) => console.log(res));

async/await 的实现原理就是 generator 自动执行器 + promise

// generator 自动执行器
function getNumPromise(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num);
}, 1000);
});
}

function* getNum() {
const f1 = yield getNumPromise(1);
const f2 = yield getNumPromise(f1 + 1);
return yield getNumPromise(f2 + 1);
}

function run(gen) {
// 生成一个迭代器
const g = gen();

function next(data) {
let result = g.next(data);
if (result.done) {
return result.value;
}
result.value.then((data) => {
console.log(data);
next(data);
});
}

next();
}

run(getNum);

可以通过 babel 查看转译 async 后的结果,其实跟上面内容差不多的

· 10 min read

数组是 js 中常见的对象,关于数组的操作很多,作为一个前端攻城狮,应该要能通过灵活应用其内置属性和方法,来解决一系列数组处理问题

初始化数组

  • 构造函数方法

var arr = new Array(3)

var arr = new Array('a','b','c')

var arr = Array(3)

  • 字面量方法 //无法指定成员个数

var arr = ['a','b','c']

数组检查和转化

检查数组类型

let arr = [];
// 1.ES5的isArray方法
Array.isArray(arr);

// 2.constructor指向对象实例的构造函数
arr.constructor === Array;

// 3.instanceof原理同2
arr instanceof Array;

// 4.利用Object的原型方法toString判断(必须是Object原型方法,array和function均重写了这个方法)
// toString方法返回反映这个对象的字符串
//可以用来区分各种类型,但是无法判断自定义对象类型,此时可用2.3方法判断自定义类型
Object.prototype.toString.call(arr) === "[object Array]";

数组转化问题

数组转 String--join(separator)
// 注意区别使用空格和空值
arr.join(" ");
String 转数组--split(separator)
// 根据separator来分割字符串,每一项作为数组的一个值
str.spilt(",");
对象转数组
let obj = { name: "jacksonzhou", age: 23 };
Object.keys(obj); // [name,age]
Object.values(obj); // ['jacksonzhou',23]
Object.entries(obj); // [[name,'jacksonzhou'],[age,23]]
数组转对象
Object.assign({},[1,2]) // 转成{0:1,1:1}
{...['a','b','c']} // 转成{0:'a',1:'b',3:'c'}
// es10的api,浏览器暂不支持
Object.fromEntries([name,'jacksonzhou'],[age,23])
类数组转化为数组

类数组定义:不具有数组所具有的方法,但具有 length 属性,其他属性值为非负整数;常见的类数组有 arguments 对象和 DOM 方法的返回结果(document.getElementsByTagName())

// 使用Array原型方法slice
Array.prototype.slice.call(arguments)
// from方法将类似数组或可迭代对象创建为数组
Array.from(arguments)
// ... (扩展运算符)将类数组扩展为一个列表??(列表实际上是啥?),再定义为数组。
[...arguments]
// 借助对象+indexOf,思路是遍历数组,将每一个值标记为属性,将出现一次的值加入新数组;
//一旦obj.indexOf(i)不为-1,说明重复了,不将它加入新数组

针对数组某项的操作

判断是否包含值

// indexOf
[1, 2, 3]
.indexOf(2) // 1(其对应索引),不存在则为-1
[
// includes
(1, 2, 3)
].includes(2) // true
[
// find
(1, 2, 3)
].find((i) => {
i === 3;
}) // 3,无值返回undefined
[
// findIndex
(1, 2, 3)
].findIndex((i) => {
i === 3;
}); //2,返回索引,无值返回-1

给每一项设置统一值

[1, 2, 3].fill(5); // [5,5,5]

每一项是否满足

[1, 2, 3].every((i) => {
return i > 2;
}); // false

有一项满足

[1, 2, 3].some((i) => {
return i > 2;
}); // true

计算每一项出现的次数

// 数组遍历+借助对象+属性标记+值计数

过滤数组

[1, 2, 3, 4, 5]
.filter((i) => {
return i > 2;
}) // [3,4,5]
[
// 过滤空值
(1, 2, null, 3, 0, false, 4, undefined, "", 5)
].filter(Boolean)
[
// [1,2,3,4,5],但不过滤空的引用类型,比如{}、[]
("1", "2", "3")
].map(parseInt) ===
// 输出[1, NaN, NaN]
["1", "2", "3"].map((item, index) => parseInt(item, index));
  • 简易实现代码
Array.prototype.filterz = function (fn) {
let arr = this;
let newArr = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
newArr.push(arr[i]);
}
}
return newArr;
}[(1, 2, 3, 4, 5)].filterz((i) => {
return i > 2;
});

重装数组

[1, 2, 3].map((i) => {
return "我是" + i;
});

数组乱序

let arr = [1,2,3,4,5,6]
arr.sort(function(){
return Math.random()-0.5
})

数组去重

Map 对象+过滤器 filter

Map 数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

function unique(arr) {
const res = new Map();
return arr.filter((a) => !res.has(a) && res.set(a, 1));
}

set(集合)去重

Set 数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。

Array.from(new Set([1,2,2,3]))
[...new Set([1,2,2,3])]

直接使用数组去重

function unique2(arr) {
let newArr = []
for(let i=0;i<arr.length;i++){
if(newArr.indexOf(arr[i])===-1)newArr.push(arr[i])
}
return newArr
}

对象数组去重

let json1 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];

let json2 = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 4, name: "d" },
];

let json = json1.concat(json2); //两个数组对象合并
let newJson = []; //盛放去重后数据的新数组
for (item1 of json) {
//循环json数组对象的内容
let flag = true; //建立标记,判断数据是否重复,true为不重复
for (item2 of newJson) {
//循环新数组的内容
if (item1.id == item2.id) {
//让json数组对象的内容与新数组的内容作比较,相同的话,改变标记为false
flag = false;
}
}

if (flag) {
//判断是否重复
newJson.push(item1); //不重复的放入新数组。 新数组的内容会继续进行上边的循环。
}
}

console.log("newJson", newJson);

扁平化数组

// flat是ES10的api, n表示维度, n为 Infinity 时维度为无限大
[[1,2],3].flat(2) // [1,2,3]
[1,[2,3,[4,[...]]]].flat(Infinity) // [1,2,3,4,...]
// 利用toString()实现
let arr = [1,[2,3,[4,5]]]
arr.toString().split(',').map(item=>{
+item // 强制转换为数字类型
})
// 使用迭代的方式实现
let arr = [1, 2, [3, 4, 5, [6, 7], 8]]
function flat(arr) {
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr) // 将数组型数值追加,并重复追加,知道没有数组型数据
}
return arr
}
flat(arr)
// 使用递归的方式实现
const flat = arr => arr.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flat(cur)] : [...acc, cur]), [])

排序

// sort--关于排序有太多算法知识点,这里只简单实现升序降序,后续再总结相关算法吧~
[1, 3, 2]
.sort((a, b) => {
a - b;
}) // 升序
[(1, 3, 2)].sort((a, b) => {
b - a;
}); // 降序

求最值

// Math.max/min
Math.max(...[1, 2, 3]); // ...将数组转化为参数列表,供max方法使用
Math.max.apply(this, [1, 2, 3])[
// 利用apply可带数组参数
// === Math.max.call(this,...[1,2,3])

// reduce--累加器
(1, 2, 3, 5)
].reduce((pre, cur) => {
return Math.max(pre, cur);
}, 0);
// pre 指上一次return的值,cur指当前值,0指初始值

reducer 累加器原理

function reduce (arr, fn, initVal) {
for (let i = 0; i < arr.length; i++) {
initVal = fn(initVal, arr[i])
}
return initVal
}
let newArr = reduce(arr, (sum, cur) => {
return sum + cur
}, 5)
console.log(newArr)

求和

// 遍历求和
let sum = 0;
[1, 2, 3].map((i) => {
sum += i;
});
console.log(sum);
// reduce -- 累加器
let sum = [1, 2, 3].reduce((prev, current) => {
return prev + current;
}, 0);
// 0 是初始值, prev 是先前return的值,current是当前值

合并

// concat--不会改变原数组,返回一个数组副本
let newArr = arr.concat([1,2,3])
// push--会改变原数组
arr.push([1,2,3])
// ...
[...[1,2,3],...[4,5]]

其他

  • 请把两个数组 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']['A', 'B', 'C', 'D'],合并为 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']
// 先给第二个数组的值加个数字,让其能按顺序排序,然后再遍历数组去掉第二个数组的值加上的数字
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})

let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})

栈和队列操作

栈操作

  • push()
  • pop()

队操作

  • push()
  • shift() // 区别一下 unshift()

扩展运算符...

  • 复制数组
let arr1 = [1, 2, 3];
let arr2 = arr1; // 仅是复制引用,修改arr2会影响arr1
let arr2 = [...arr1]; // 实际上等同于重新声明赋值
  • 转为函数参数
Math.max(...[-1, 1, 1001, -52]); // 1001

· 13 min read

ECMAScript6 是 JavaScript 的标准,目前浏览器的 JavaScript 大多数都支持 ES6。ES6 使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

var,let 和 const

  • var 和 let 都可用于定义变量,但是 let 定义的变量只能在一个作用域内使用
{
var demo_1 = "demo_1";
let demo_2 = "demo_2";
}
console.log(demo_1);
// 可以打印出来demo_1
console.log(demo_2);
// 不可以打印出来demo_2
  • const 定义不可变量
const demo_3 = "demo_3";
demo_3 = "demo_4";
// 会提示报错
// 如果 const 的是一个对象,对象所包含的值是可以被修改的。
// 抽象一点儿说,就是对象所指向的地址不能改变,而变量成员是可以修改的。

js 在引擎扫描代码时,如果发现变量声明,用 var 声明变量时会将声明提升到函数或全局作用域的顶部。但是 let 或者 const,会将声明关进一个小黑屋也是 TDZ(暂时性死区),只有执行到变量声明这句语句时,变量才会从小黑屋被放出来,才能安全使用这个变量

字符串模板

  • 告别用 + 和 \ 拼接字符串
// 在ES5时我们通过反斜杠(\)来做多行字符串或者字符串一行行拼接
// ES6直接通过``拼接
const str = "demo";
console.log(`
test ${str}!!
`);
// 显示test demo!!
  • 其他好用的方法
// includes 判断字符串是否包含指定值
const str = "jacksonzhou";
console.log(str.includes("zh")); // true

// repeat 获取字符串重复的次数
const str = "z";
console.log(str.repeat(3)); // 'zzz'
//如果你带入小数, Math.floor(num) 来处理
// s.repeat(3.1) 或者 s.repeat(3.9) 都当做成 s.repeat(3) 来处理

// startsWith 和 endsWith 判断是否以给定文本开始或者结束
const str = "hello world!";
console.log(str.startsWith("hello")); // true
console.log(str.endsWith("!")); // true

// padStart 和 padEnd 填充字符串,应用场景:时分秒
setInterval(() => {
const now = new Date();
const hours = now.getHours().toString();
const minutes = now.getMinutes().toString();
const seconds = now.getSeconds().toString();
console.log(
`${hours.padStart(2, 0)}:${minutes.padStart(2, 0)}:${seconds.padStart(
2,
0
)}`
);
}, 1000);
  • 面试题
    • 模拟一个模板字符串的实现
    • 实现标签化模板(自定义模板规则)

函数扩展

  • 函数简写 (箭头函数)
    • 不需要 function 关键字来创建函数
    • 可省略 return 关键字
    • 继承当前上下文的 this 关键字
    • 没有 this、super、arguments 和 new.target 绑定,这些值由继承的上下文决定
    • 不存在 prototype 这个属性
//例如:
[1, 2, 3]
.map((x) => x + 1)
[
//等同于:
(1, 2, 3)
].map(
function (x) {
return x + 1;
}.bind(this)
);

什么情况下不适用箭头函数? 当要求动态上下文的时候,你就不能使用箭头函数,比如:定义对象方法、定义原型方法、定义构造函数、定义事件回调函数 什么时候你不能用箭头函数

  • 带默认参数
// es6之前,定义默认参数的方法是在一个方法内部定义
var link = function (width, height) {
var width = width || 100;
var height = height || 100;
};
// 而在es6中你可以这样写
var link = function (width = 100, height = 100) {};
// 做参数的非空检测
const notNull = () => {
throw new Error("参数不能为空");
};
const func = (id = notNull()) => {
console.log(`${id}`);
};
func(1);
func();

展开运算符 ...

// 数组
let arr = [1, 2];
console.log(...arr, 3, 4, 5);
// 打印出1,2,3,4,5
const number = [1, 2, 3, 4, 5];
const [first, ...rest] = number;
console.log(rest); //2,3,4,5
// 对象
const alp = { fist: "a", second: "b" };
const alphabets = { ...alp, third: "c" };
console.log(alphabets); //{ "fist": "a", "second": "b", "third": "c"
// 函数参数的应用
const add_4 = (num1, num2) => num1 + num2;
console.log(add_4(...arr));
// 展开1和2,分别作为num1和num2的参数,打印出3

// 对于对象而言,组合成新的对象时,如果有重复的属性名,后者会覆盖前者
const first = {
a: 1,
b: 2,
c: 6,
};
const second = {
c: 3,
d: 4,
};
const total = { ...first, ...second };
console.log(total); // { a: 1, b: 2, c: 3, d: 4 }

对象扩展

  • 对象简写
const demo = "objs";
const obj = {
name: "obj",
func:function() {
return: 1;
},
demo, // 相当于demo: demo,即键值对重名简写
}
console.log(obj)
// 打印出来的对象不含有objs属性,而是demo:"objs"
// 必须在外面定义 obj[demo] ="jack"才能使对象具有objs属性
const demo = "objs";
const obj = {
name: "obj",
func() {
return 1;
},
[demo]: "jack",
[demo+"2"]: "jackson",
}
console.log(obj)
// 打印出来的对象含有objs属性和objs2属性,即objs:"jack"

  • 对象方法

    • ES6 对象提供了 Object.assign()这个方法来实现浅复制。
    const objA = { name: "cc", age: 18 };
    const objB = { address: "beijing" };
    const objC = {}; // 这个为目标对象
    const obj = Object.assign(objC, objA, objB);

    // 我们将 objA objB objC obj 分别输出看看
    console.log(objA); // { name: 'cc', age: 18 }
    console.log(objB); // { address: 'beijing' }
    console.log(objC); // { name: 'cc', age: 18, address: 'beijing' }
    console.log(obj); // { name: 'cc', age: 18, address: 'beijing' }

    // 是的,目标对象ObjC的值被改变了。
    // so,如果objC也是你的一个源对象的话。请在objC前面填在一个目标对象{}
    Object.assign({}, objC, objA, objB);
  • 面试题

// 请使用 ES6 重构一下代码
// 第一题
var jsonParse = require("body-parser").jsonParse;
// 答
import { jsonParse } from "body-parser";
// 第二题
var body = request.body;
var username = body.username;
var password = body.password;
// 答
const {
body,
body: { username, password },
} = request;
// 这个在配合async和await处理异步请求时很好用

解构赋值

  • 数组赋值
let arr = [1, 2];
let [num1, num2] = arr;
<!--num1和num2分别赋值为12-->
  • 对象赋值
let obj = {num: 1, type: "对象"};
let {num, type} = obj;
<!--num和type分别赋值为1"对象"-->

class 类(对象封装的语法糖)

原理实现可看 从 js 看面向对象的封装、继承和多态

class Person {
constructor(name) { // name为实例传入的参数
this.name = name
}
hello() {
return 'Hello, I am ' + this.name + '.'
}
}
class Actor extends Person {
hello() {
return super.hello() + ' I am an actor.'
}
}
var jacksonzhou = new Actor('jacksonzhou')
jacksonzhou.hello()

// 定义类的变量名
const testClass = class demo{
constructor(){}
}
// 可省略类名
const testClass = class {
constructor(){}
}

// 立即执行的类
const testClass = new class {
constructor(name,age){
console.log(name,age)
}
}("jacksonzhou",24)
  • constructor(arguments)
constructor中定义的属性称为实例属性
constructor外声明的属性定义在原型上,即原型属性
使用new创建实例对象时,自动调用这个函数,传入参数,只能有一个constructor
  • extends
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))
ES6的继承,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

  • super
1.子类constructor中需要执行super(arguments),要放在头部
2.因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工
3.通过使用super().methods 引用父类方法或属性
4.super作为对象时,指向父类的原型对象(super.getName())

模块化

将模块标准(AMD,CommonJS,RequireJS)化为通用格式

  • import, import{}, export, export default
1.当用export default people导出时,就用 import people 导入(不带大括号)
2.当用export name 时,就用import { name }导入(记得带上大括号)
3.一个文件里,有且只能有一个export default,但可以有多个export
4.当一个文件里,既有一个export default people, 又有多个export name 或者 export age时,导入就用 import people, { name, age }
5.当一个文件里出现n多个 export 导出很多模块,导入时除了一个一个导入,也可以用import * as example
  • node 还不支持 import,需要用 require 加载文件

Promise

在 promise 之前代码过多的回调或者嵌套,可读性差、耦合度高、扩展性低。通过 Promise 机制,扁平化的代码机构,大大提高了代码可读性;用同步编程的方式来编写异步代码,保存线性的代码逻辑,极大的降低了代码耦合性而提高了程序的可扩展性。

// 其实就是用同步的方式去实现异步
fetch('/api')
.then(res => res.json())
.then(data => ({ data }))
.catch(err => ({ err }))
  • 面试题
setTimeout(function () {
console.log(1);
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
});
console.log(5);

/* 在执行定时器、事件、ajax等操作的时候,会把异步操作放到任务队列里等待主线程执行完
* 所以,定时器(setTimeout)会放到任务队列中,代码继续往下走
* 先输出2、3
* promise中的then操作是放在执行栈,也就是在主线程的最后去执行
* 主线程会继续往下走
* 所以输出5、4
* 执行完主线程的任务,才会去执行任务队列中的任务
* 所以最后执行1
*/

Generators 和 yield

生成器( generator)是能返回一个迭代器的函数。生成器函数也是一种函数,最直观的表现就是比普通的 function 多了个星号*,在其函数体内可以使用 yield 关键字,有意思的是函数会在每个 yield 后暂停

// 生成器
function* createIterator() {
yield 1;
yield 2;
yield 3;
}

// 生成器能像正规函数那样被调用,但会返回一个迭代器
let iterator = createIterator();

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

async 和 await

以顺序、同步代码的编写方式来控制异步流程,需要注意以下几点:

  • await 关键字必须位于 async 函数内部
  • await 关键字后面需要一个 promise 对象(不是的话就调用 resolve 转换它)
  • 串行的执行异步操作,实现并行可以考虑promise.all
async getUser () {
const res = await Apis.getUser()
// 未返回的情况下会阻塞代码,res为异步请求返回的结果
console.log(res)
}

参考

· 13 min read

首先明确一下这几个概念

  • 函数是一种可调用的对象,对象都是通过函数创建的
  • 每一个函数都有一个原型对象, 每一个原型对象都有一个指向构造函数的指针;
  • 实例包含了一个指向原型对象的内部(不可见的)指针;
  • 某个对象的原型链,就是由该对象开始,通过proto属性连接起来的一串对象

创建对象

  • 字面量形式 -- 简单快速,但限制了对象的重用
  • 工厂模式 -- 可重复创建对象,但无法识别对象类型,且公共方法 eat 重复了多次
function Cat() {
var cat = new Object()
cat.name = 'JacksonZhou'
cat.eat = function() {
console.log('吃鱼')
}
return cat
}
let lazyCat = Cat()
  • 构造函数 -- 可识别对象类型,但是仍然不能共享公共函数(eat)
function Cat() {
this.name = 'JacksonZhou'
this.eat = function() {
console.log('吃鱼')
}
}
let lazyCat = new Cat()
  • 原型方式 -- 能共享公共函数,动态地添加原型对象的方法和属性,并直接反映在对象实例上;但不能通过构造函数传参;属性是引用类型时,被多个实例共享容易产生属性篡改的问题
function Cat() {}
Cat.prototype.name = 'JacksonZhou'
Cat.prototype.eat = function(){
console.log('吃鱼')
}
let lazyCat = new Cat()
  • 构造函数和原型的混合模式 -- 可对函数传参;在构造函数内定义实例属性(包括引用类型),而公共属性和方法则利用原型模式定义;直接通过对象字面量给 Person.prototype 进行赋值的时候会导致 constructor 改变;并且通过对象字面量给 Person.prototype 进行赋值,会无法作用在之前创建的对象实例上
function Cat(name, food) {
this.name = name
this.food = food
}
Cat.prototype.eat = function() {
console.log('this.food')
}
let lazyCat = new Cat('Tom', 'fish')
  • 动态原型方式 -- 在混合模式基础上,将所有信息封装在了构造函数中;先判断公共函数是否已存在于实例中
function Cat() {
this.name = name
if(typeof this.eat !== 'function') {
Cat.prototype.eat = function() {
console.log('fish')
}
}
}

参考自-->js 创建对象的方法和优缺点

属性/方法

实例属性方法

绑定在将来通过构造函数创建的实例上,并且需要通过这个实例来访问的属性、方法

静态属性方法

绑定在构造函数上的属性方法,需要通过构造函数访问

// 比如我们想取出这个Person构造函数创建了多少个实例
function Person(name, age) {
this.name = name;
this.age = age;
if (!Person.total) {
Person.total = 0;
}
Person.total++;
}

let p1 = new Person("jacksonzhou", 28);
console.log(Person.total); // 1
let p2 = new Person("swr", 28);
console.log(Person.total); // 2

原型属性方法

构造函数 new 出来的实例,都共享这个构造函数的原型对象上的属性方法,类似共享库。

对象(Object)属性

  • __proto__或者[[prototype]]指针,指向构造函数的原型。
  • constructor:默认指向创建当前对象的构造函数

每一个函数新建的时候都有一个默认的 prototype, prototype 这个对象上面默认有一个指向自己的 constructor,所以要注意,当你的原型 prototype 被改了, 实例的 constructor 就变了

对象常用方法

1.hasOwnProperty(prop):检查给定的属性是否在当前对象实例中(而不是在实例的原型中)。作为参数的属性必须以 string 形式指定

function Person() {
this.name = "jacksonzhou";
}
let per = new Person();

let key = "name";
if (key in p && per.hasOwnProperty(key)) {
// name存在且仅存在于p对象中
}

2.isPrototypeOf(object):用于检查传入的对象是否是另一个对象的原型。

function Person() {
this.name = "jacksonzhou";
}
let per = new Person();

let obj = Person.prototype;
obj.isPrototypeOf(per); // obj是per的原型,返回true

3.propertyIsEnumerable(propertyName):用于检查给定的属性是否能枚举,即是否能够使用 for in 语句

4.toLocaleString():返回对象的字符串表示,与环境的地区对应

5.toString():返回对象的字符串表示

6.valueOf(): 返回对象的字符串、number、Boolean 表示

new 创建实例对象的四个步骤

var func = function () {
// 加入是function func()呢?步骤会不会改变
this.name = "jacksonzhou";
};
var newFunc = new func();
  • 创建空对象
var obj = new Object();
  • 将空对象的__proto__指向构造函数的prototype
obj.__proto__ = func.prototype;
  • 使用空对象作为上下文(this 指向),并执行构造函数
// 此时空对象里并没有name属性,执行构造函数,将创建name属性
var result = func.call(obj);
  • 返回新对象
// 判断是值类型还是引用类型,引用类型直接返回相应的值;值类型则返回新对象即可
if(typeof(result) === 'object') {
func = result
} else {
func = obj
}

new 升级版实现

function new(Con, ...args) {
let obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}

三大特性

面向对象三大特性就是封装继承和多态,简单理解,对于猫这种动物,它本身就是一个封装好的类,你只需要供它吃喝(输入),它就能表现猫的行为(输出),同时它继承了动物所具有的习性(吃东西等~),而不同的猫因为所处环境或者习性的不同,可能会有不同的表现和行为,这就是多态。

封装

把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

① 在 ES6 之前,没有 class 这个概念,借由原型对象和构造函数来实现

function Cat(name, food) {
this.name = name; // 公有属性
this.food = food;
}
Cat.prototype.say = function () {
// 公有方法
console.log(this.name + " likes eating " + this.food);
};
Cat.see = function () {
console.log("这是静态方法,无需实例化可调用");
};
var cat = new Cat("Lazier", "mouse");
cat.say(); // 实例共享原型属性和方法

② ES6 的 class

class Cat {
constructor(name, food) {
this.name = name;
this.food = food;
}
static see() {
console.log("这是静态方法,无需实例化可调用");
}
say() {
console.log(this.name + " likes eating " + this.food);
}
}
var cat = new Cat("Lazier", "mouse");
cat.say();

以上 class 的基本实现原理如下 ↓

var Cat = function(){
function Cat(name, food){
this.name = name
this.food = food
}
// 执行挂载函数,创建类
createClass(Cat,[{key:"say",value:function(){
console.log(this.name+" likes eating " + this.food)
}}],[{key:"see",value:function(){
console.log('这是静态方法,无需实例化可调用')}])
}

// 定义对象属性
let defineProperties = function(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
Object.defineProperty(target, descriptor.key, descriptor)
}
}
// 挂载函数,将静态或动态方法分别挂载到Cat和Cat的prototype上
var createClass = function({
return function(Constructor,protoProps,staticProps){
if(protoProps){ // 原型方法
defineProperties(Constructor.prototype,protoProps)
}
if(staticProps){ // 静态方法
defineProperties(Constructor,staticProps)
}
}
})

了解面向对象的公有、私有、静态属性和方法可以看下面这篇文章的总结

js 面向对象之公有、私有、静态属性和方法详解

继承

子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

js 实现继承有多种方式

  • 原型链继承
// 将子类的prototype指向父类的实例
function Parent() {}
function Son() {}
Son.prototype = new Parent();
// * 把Son的原型对象的constructor指向Son,解决类型判断问题
Son.prototype.constructor = Son;
  • 借助构造函数继承(使用 call 和 apply 实现继承)
function Parent() {}
function Son() {
// 将父类函数中的this,强行绑定为子类的this
// 可传参
Parent.call(this, arguments);
}
  • 组合继承
// 原型属性方法由原型链实现继承,实例属性方法由借用构造函数实现继承
// 这样,在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性
function Parent(name) {
this.name = name;
}
function Son(name, age) {
// 继承父类的实例属性方法,之后再添加自己的实例属性方法
Parent.call(this, name);
this.age = age;
}
Son.prototype = new Parent();
// 重写Son的原型对象
Son.prototype.constructor = Son;

var demo = new Son("jacksonzhou", 23);
  • 寄生式组合继承 -- 现在最常用的继承方法
// 获得父类原型属性方法的副本,解决组合继承的属性重复问题
function inheritPrototype(son, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = son;
son.prototype = prototype;
}

function Parent(name) {
this.name = name;
}
function Son(name, age) {
Parent.call(this, name);
this.age = age;
}
Son.prototype = inheritPrototype(Son, Parent);

var demo = new Son("jacksonzhou", 23);

多态

同一操作用在不同对象上,可以产生不同的解释和不同的执行结果

var makeSound = function (animal) {
animal.sound();
};
// 声明狗的构造函数
var Dog = function () {};
Dog.prototype.sound = function () {
console.log("汪汪汪");
};
// 声明猫的构造函数
var Cat = function () {};
Cat.prototype.sound = function () {
console.log("喵喵喵");
};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 非多态写法
var makeSound = function (animal) {
if (animal instanceof Dog) {
console.log("汪汪汪");
} else if (animal instanceof Cat) {
console.log("喵喵喵");
}
};
var Dog = function () {};
var Cat = function () {};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 很明显,后续有其他动物加入都要去修改makeSound函数,很不优雅!

这里要介绍下方法重载

方法重载是让类以统一的方式处理不同类型数据的一种手段。表现为多个同名函数同时存在,但具有不同的参数个数或类型。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这也是一种多态性。

其实 js 本身并没有这个概念,但我们可以通过操作参数的类数组arguments,根据该类数组的长度以及其元素的类型来选择不同的实现,来模拟实现函数重载效果

// js的函数参数相当灵活~可理解成一个动态的类数组
// 不加参数,调用时有传入参数也不会报错
function countCat() {
if (arguments.length == 1) {
console.log(`这是一只猫,${arguments[0]}`);
} else if (arguments.length == 2) {
console.log(`这是两只猫,${arguments[0]}${arguments[1]}`);
} else {
console.log("没猫了~");
}
}
countCat();
countCat("Tom");
countCat("Tom", "Mary");