木易杨

木易杨 查看完整档案

上海编辑北京航空航天大学  |  软件 编辑哈啰出行  |  资深前端工程师 编辑 muyiy.cn 编辑
编辑

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个面试重难点。

个人动态

木易杨 发布了文章 · 2019-07-10

前端 100 问:能搞懂80%的请把简历给我

引言

半年时间,几千人参与,精选大厂前端面试高频 100 题,这就是「壹题」。

在 2019 年 1 月 21 日这天,「壹题」项目正式开始,在这之后每个工作日都会出一道高频面试题,主要涵盖阿里、腾讯、头条、百度、网易等大公司和常见题型。得益于大家热情参与,现在每道题都有很多答案,提供的解题思路和答案也大大增长了我的见识,到现在已累积 100 道题目,『 8000+ 』Star 了,可以说你面试中遇到过的题目,在这里肯定能发现熟悉的身影。

后期计划除了持续更新「壹题」之外,还将整理非常详细的答案解析,提供完整的思考链路,帮助大家更好的理解题目,以及题目背后的知识,「我们的目标不是背题,而是通过题目查漏补缺,温故知新」。

更多更全更详细的每日一题和答案解析,戳这里查看

第 1 - 10 题

第 1 题:(滴滴、饿了么)写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

解析:第 1 题

<br/>

第 2 题:['1', '2', '3'].map(parseInt) what & why ?

解析:第 2 题

<br/>

第 3 题:(挖财)什么是防抖和节流?有什么区别?如何实现?

解析:第 3 题

<br/>

第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

解析:第 4 题

<br/>

第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现?

解析:第 5 题

<br/>

第 6 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?

解析:第 6 题

<br/>

第 7 题:ES5/ES6 的继承除了写法以外还有什么区别?

解析:第 7 题

<br/>

第 8 题:setTimeout、Promise、Async/Await 的区别

解析:第 8 题

<br/>

第 9 题:(头条、微医)Async/Await 如何通过同步的方式实现异步

解析:第 9 题

<br/>

第 10 题:(头条)异步笔试题

请写出下面代码的运行结果
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

解析:第 10 题

<br/>

第 11 - 20 题

第 11 题:(携程)算法手写题

已知如下数组:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

解析:第 11 题

<br/>

第 12 题:(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点。

解析:第 12 题

<br/>

第 13 题:(微医)Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

解析:第 13 题

<br/>

第 14 题:(兑吧)情人节福利题,如何实现一个 new

解析:第 14 题

<br/>

第 15 题:(网易)简单讲解一下http2的多路复用

解析:第 15 题

<br/>

第 16 题:谈谈你对TCP三次握手和四次挥手的理解

解析:第 16 题

<br/>

第 17 题:A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态

如果A 与 B 建立了正常连接后,从未相互发过数据,这个时候 B 突然机器重启,问 A 此时处于 TCP 什么状态?如何消除服务器程序中的这个状态?(超纲题,了解即可)

解析:第 17 题

<br/>

第 18 题:(微医)React 中 setState 什么时候是同步的,什么时候是异步的?

解析:第 18 题

<br/>

第 19 题:React setState 笔试题,下面的代码输出什么?

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

解析:第 19 题

<br/>

第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

解析:第 20 题

<br/>

第 21 - 30 题

第 21 题:有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

解析:第 21 题

<br/>

第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

解析:第 22 题

<br/>

第 23 题:介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景

解析:第 23 题

<br/>

第 24 题:聊聊 Redux 和 Vuex 的设计思想

解析:第 24 题

<br/>

第 25 题:说说浏览器和 Node 事件循环的区别

解析:第 25 题

<br/>

第 26 题:介绍模块化发展历程

可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module"> 这几个角度考虑。

解析:第 26 题

<br/>

第 27 题:全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?。

解析:第 27 题

<br/>

第 28 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?

解析:第 28 题

<br/>

第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

解析:第 29 题

<br/>

第 31 - 40 题

第 30 题:两个数组合并成一个数组

请把两个数组 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'] 和 ['A', 'B', 'C', 'D'],合并为 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']。

解析: 第 30 题

<br/>

第 31 题:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

for (var i = 0; i< 10; i++){
    setTimeout(() => {
        console.log(i);
    }, 1000)
}

解析:第 31 题

<br/>

第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

解析:第 32 题

<br/>

第 33 题:下面的代码打印什么内容,为什么?

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();

解析:第 33 题

<br/>

第 34 题:简单改造下面的代码,使之分别打印 10 和 20。

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();

解析:第 34 题

<br/>

第 35 题:浏览器缓存读取规则

可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?

解析:第 35 题

<br/>

第 36 题:使用迭代的方式实现 flatten 函数。

解析:第 36 题

<br/>

第 37 题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?

解析:第 37 题

<br/>

第 38 题:(京东)下面代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
     console.log(1);
}

解析:第 38 题

<br/>

第 39 题:介绍下 BFC 及其应用。

解析:第 39 题

<br/>

第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop

如果修改了,Vue 是如何监控到属性的修改并给出警告的。

解析:第 40 题

<br/>

第 41 - 50 题

第 41 题:下面代码输出什么

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()

解析:第 41题

<br/>

第 42 题:实现一个 sleep 函数

比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现

解析:第 42 题

<br/>

第 43 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

解析:第 43 题

<br/>

第 44 题:介绍 HTTPS 握手过程

解析:第 44 题

<br/>

第 45 题:HTTPS 握手过程中,客户端如何验证证书的合法性

解析:第 45 题

<br/>

第 46 题:输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

解析:第 46 题

<br/>

第 47 题:双向绑定和 vuex 是否冲突

解析:第 47 题

<br/>

第 48 题:call 和 apply 的区别是什么,哪个性能更好一些

解析:第 48 题

<br/>

第 49 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

解析:第 49 题

<br/>

第 50 题:(百度)实现 (5).add(3).minus(2) 功能。

例: 5 + 3 - 2,结果为 6

解析:第 50 题

<br/>

第 51 - 60 题

第 51 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

解析:第 51 题

<br/>

第 52 题:怎么让一个 div 水平垂直居中

解析:第 52 题

<br/>

第 53 题:输出以下代码的执行结果并解释为什么

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x)     
console.log(b.x)

解析:第 53 题

<br/>

第 54 题:冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?

解析:第 54 题

<br/>

第 55 题:某公司 1 到 12 月份的销售额存在一个对象里面

如下:{1:222, 2:123, 5:888},请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]。

解析:第 55 题

<br/>

第 56 题:要求设计 LazyMan 类,实现以下功能。

LazyMan('Tony');
// Hi I am Tony

LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

解析:第 56 题

<br/>

第 57 题:分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。

解析:第 57 题

<br/>

第 58 题:箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

解析:第 58 题

<br/>

第 59 题:给定两个数组,写一个方法来计算它们的交集。

例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。

解析:第 59 题

<br/>

第 60 题:已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。

<img data-original="1.jpg" style="width:480px!important;”>

解析:第 60 题

<br/>

第 61 - 70 题

第 61 题:介绍下如何实现 token 加密

解析:第 61 题

<br/>

第 62 题:redux 为什么要把 reducer 设计成纯函数

解析:第 62 题

<br/>

第 63 题:如何设计实现无缝轮播

解析:第 63 题

<br/>

第 64 题:模拟实现一个 Promise.finally

解析:第 64 题

<br/>

第 65 题: a.b.c.da['b']['c']['d'],哪个性能更高?

解析:第 65 题

<br/>

第 66 题:ES6 代码转成 ES5 代码的实现思路是什么

解析:第 66 题

<br/>

第 67 题:数组编程题

随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20],将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]

解析:第 67 题

<br/>

第 68 题: 如何解决移动端 Retina 屏 1px 像素问题

解析:第 68 题

<br/>

第 69 题: 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC' 变成 'aBc' 。

解析:第 69 题

<br/>

第 70 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的

解析:第 70 题

<br/>

第 71 - 80 题

第 71 题: 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。

解析:第 71 题

<br/>

第 72 题: 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。

image-20190512225510941

解析:第 72 题

<br/>

第 73 题: 介绍下 BFC、IFC、GFC 和 FFC

解析:第 73 题

<br/>

第 74 题: 使用 JavaScript Proxy 实现简单的数据绑定

解析:第 74 题

<br/>

第 75 题:数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少

解析:第 75 题

<br/>

第 76 题:输出以下代码运行结果

// example 1
var a={}, b='123', c=123;  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');  
a[b]='b';
a[c]='c';  
console.log(a[b]);

---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};  
a[b]='b';
a[c]='c';  
console.log(a[b]);

解析:第 76 题

<br/>

第 77 题:算法题「旋转数组」

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: [1, 2, 3, 4, 5, 6, 7] 和 k = 3
输出: [5, 6, 7, 1, 2, 3, 4]
解释:
向右旋转 1 步: [7, 1, 2, 3, 4, 5, 6]
向右旋转 2 步: [6, 7, 1, 2, 3, 4, 5]
向右旋转 3 步: [5, 6, 7, 1, 2, 3, 4]

示例 2:

输入: [-1, -100, 3, 99] 和 k = 2
输出: [3, 99, -1, -100]
解释: 
向右旋转 1 步: [99, -1, -100, 3]
向右旋转 2 步: [3, 99, -1, -100]

解析:第 77 题

<br/>

第 78 题:Vue 的父组件和子组件生命周期钩子执行顺序是什么

解析:第 78 题

<br/>

第 79 题:input 搜索如何防抖,如何处理中文输入

解析:第 79 题

<br/>

第 80 题:介绍下 Promise.all 使用、原理实现及错误处理

解析:第 80 题

<br/>

第 81 - 90 题

第 81 题:打印出 1 - 10000 之间的所有对称数

例如:121、1331 等

解析:第 81 题

<br/>

第 82 题:周一算法题之「移动零」

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。

解析:第 82 题

<br/>

第 83 题:var、let 和 const 区别的实现原理是什么

解析:第 83 题

<br/>

第 84 题:请实现一个 add 函数,满足以下功能。

add(1);             // 1
add(1)(2);      // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

解析:第 84 题

<br/>

第 85 题:react-router 里的 <Link> 标签和 <a> 标签有什么区别

如何禁掉 <a> 标签默认事件,禁掉之后如何实现跳转。

解析:第 85 题

<br/>

第 86 题:(京东、快手)周一算法题之「两数之和」

给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。

你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解析:第 86 题

<br/>

第 87 题:在输入框中如何判断输入的是一个正确的网址。

解析:第 87 题

<br/>

第 88 题:实现 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

// 转换后的结果如下
let result = [
    {
      id: 1,
      name: '部门A',
      parentId: 0,
      children: [
        {
          id: 3,
          name: '部门C',
          parentId: 1,
          children: [
            {
              id: 6,
              name: '部门F',
              parentId: 3
            }, {
              id: 16,
              name: '部门L',
              parentId: 3
            }
          ]
        },
        {
          id: 4,
          name: '部门D',
          parentId: 1,
          children: [
            {
              id: 8,
              name: '部门H',
              parentId: 4
            }
          ]
        }
      ]
    },
  ···
];

解析:第 88 题

<br/>

第 89 题:设计并实现 Promise.race()

解析:第 89 题

<br/>

第 90 题:实现模糊搜索结果的关键词高亮显示

<img data-original="https://ws3.sinaimg.cn/large/...; height="800"/>

解析:第 90 题

<br/>

第 91 - 100 题

第 91 题:介绍下 HTTPS 中间人攻击

解析:第 91 题

<br/>

第 92 题:已知数据格式,实现一个函数 fn 找出链条中所有的父级 id

const value = '112'
const fn = (value) => {
...
}
fn(value) // 输出 [1, 11, 112]

<img data-original="https://ws1.sinaimg.cn/large/...; height="800"/>

解析:第 92 题

<br/>

第 93 题:给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log(m+n))。

示例 1:

nums1 = [1, 3]
nums2 = [2]

中位数是 2.0

示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

中位数是(2 + 3) / 2 = 2.5

解析:第 93 题

<br/>

第 94 题:vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

解析:第 94 题

<br/>

第 95 题:模拟实现一个深拷贝,并考虑对象相互引用以及 Symbol 拷贝的情况

解析:第 95 题

<br/>

第 96 题:介绍下前端加密的常见场景和方法

解析:第 96 题

<br/>

第 97 题:React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的?

解析:第 97 题

<br/>

第 98 题:(京东)写出如下代码的打印结果

function changeObjProperty(o) {
  o.siteUrl = "http://www.baidu.com"
  o = new Object()
  o.siteUrl = "http://www.google.com"
} 
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);

解析:第 98 题

<br/>

第 99 题:(bilibili)编程算法题

用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。如:输入整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量,输入函数必须只有一个参数传入,必须返回字符串。

解析:第 99 题

<br/>

第 100 题:(京东)请写出如下代码的打印结果

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

解析:第 100 题

<br/>

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-
  2. 关注我的 GitHub,让我们成为长期关系
  3. 关注公众号「高级前端进阶」,每周重点攻克一个前端面试重难点,公众号后台回复「资料」 送你精选前端优质资料。

查看原文

赞 102 收藏 80 评论 1

木易杨 赞了文章 · 2019-07-03

docker+daocloud实现前端项目自动构建部署

项目的自动化部署在大公司或独角兽中用得比较多,相比来进行手动部署项目来说会更加高效。那么本文结合之前学习的docker知识点以及nginx来简单实现VueJs项目的自动部署,当然针对其他项目也类似。

运行环境

  1. 首先需要在服务器上进行docker、nginx、node等的安装。便于进行后续的操作。
  2. 通过docker拉取nginx镜像,命令docker pull nginx

通过vue-cli初始化一个项目

可以通过vue init webpack 项目名称初始化一个项目,这里假设项目名称是docker-vue,然后在该项目的根目录新建一个Dockerfile文件,大致内容如下:

FROM nginx:latest
#把当前打包工程的html复制到虚拟地址
COPY dist/ /usr/share/nginx/html/
#使用自定义nginx.conf配置端口和监听
RUN rm /etc/nginx/conf.d/default.conf
ADD default.conf /etc/nginx/conf.d/

RUN /bin/bash -c 'echo init ok!!!'

并新建一个default.conf文件,内容如下:

server {
# 项目中定义的端口号
listen       8080;
server_name  localhost;

#charset koi8-r;
#access_log  /var/log/nginx/log/host.access.log  main;

location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
}

#error_page  404              /404.html;

# redirect server error pages to the static page /50x.html
#
error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   html;
}
}

项目目录浏览
自此,基本工作已完成,接下来就是对于daocloud.io的基本配置操作了

daocloud.io基本配置操作

若无账号,可以先进行注册daocloud.io
接下来的操作分为:

  • 创建项目
  • 集群管理
  • 创建镜像仓库

创建项目

创建项目
这里需要添加项目名称,设置代码源(可以是github、gitlab)等,然后选择你所需要构建的项目,我这里选择了我自己的github仓库docker-vue,然后点击开始创建就行。
项目列表

集群管理

集群管理的主要目的是为了链接远程服务器,并通过命令进行daocloud.io镜像的创建。
新建主机
选择新建主机
新建主机2

由于我自己是购买了阿里云服务器了,系统是ubuntu的,所以就选择了这样的配置,然后在服务器上运行:

curl -sSL https://get.daocloud.io/daomonit/install.sh | sh -s e2fa03ebead51076411388c26dff2257dae89768 

来构建一个docker镜像,如:

查看镜像
主机创建成功,如下图所示:

主机创建成功

创建镜像仓库

进入【镜像仓库】选择刚才手动构建出来的镜像,并部署最新版本到自由主机或者云端测试环境

部署
创建应用
然后在进行下面的应用设置:

应用设置

部署完成

部署完成后,可通过服务器ip+刚才设置的容器端口号进行访问了。
访问
这样我们就完成了大部分的操作了,可以通过查看下docker的容器,就可以看到我们刚才创建镜像仓库成功后,就自动创建了一个容器了:
自动创建了一个容器
缺点:
利用daocloud与docker来实现自动化部署,每一次部署会增多一个docker容器,这样导致相关容器增多,不便于后期的排查。而且一些配置需要在daocloud上进行操作,也是比较花费时间。

项目测试地址:https://github.com/huangche00...

查看原文

赞 19 收藏 9 评论 0

木易杨 发布了文章 · 2019-06-13

【进阶4-3期】面试题之如何实现一个深拷贝

引言

上篇文章详细介绍了浅拷贝 Object.assign,并对其进行了模拟实现,在实现的过程中,介绍了很多基础知识。今天这篇文章我们来看看一道必会面试题,即如何实现一个深拷贝。本文会详细介绍对象、数组、循环引用、引用丢失、Symbol 和递归爆栈等情况下的深拷贝实践,欢迎阅读。

第一步:简单实现

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

根据上篇文章内容,我们可以写出简单浅拷贝代码如下。

// 木易杨
function cloneShallow(source) {
    var target = {};
    for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
}

// 测试用例
var a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    },
    a1: undefined,
    a2: null,
    a3: 123
}
var b = cloneShallow(a);

a.name = "高级前端进阶";
a.book.price = "55";

console.log(b);
// { 
//   name: 'muyiy', 
//   book: { title: 'You Don\'t Know JS', price: '55' },
//   a1: undefined,
//   a2: null,
//   a3: 123
// }

上面代码是浅拷贝实现,只要稍微改动下,加上是否是对象的判断并在相应的位置使用递归就可以实现简单深拷贝。

// 木易杨
function cloneDeep1(source) {
    var target = {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof source[key] === 'object') {
                target[key] = cloneDeep1(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

// 使用上面测试用例测试一下
var b = cloneDeep1(a);
console.log(b);
// { 
//   name: 'muyiy', 
//   book: { title: 'You Don\'t Know JS', price: '45' }, 
//   a1: undefined,
//   a2: {},
//   a3: 123
// }

一个简单的深拷贝就完成了,但是这个实现还存在很多问题。

  • 1、没有对传入参数进行校验,传入 null 时应该返回 null 而不是 {}
  • 2、对于对象的判断逻辑不严谨,因为 typeof null === 'object'
  • 3、没有考虑数组的兼容

第二步:拷贝数组

我们来看下对于对象的判断,之前在【进阶3-3期】有过介绍,判断方案如下。

// 木易杨
function isObject(obj) {
    return Object.prototype.toString.call(obj) === '[object Object]';
}

但是用在这里并不合适,因为我们要保留数组这种情况,所以这里使用 typeof 来处理。

// 木易杨
typeof null //"object"
typeof {} //"object"
typeof [] //"object"
typeof function foo(){} //"function" (特殊情况)

改动过后的 isObject 判断逻辑如下。

// 木易杨
function isObject(obj) {
    return typeof obj === 'object' && obj != null;
}

所以兼容数组的写法如下。

// 木易杨
function cloneDeep2(source) {

    if (!isObject(source)) return source; // 非对象返回自身
      
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep2(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

// 使用上面测试用例测试一下
var b = cloneDeep2(a);
console.log(b);
// { 
//   name: 'muyiy', 
//   book: { title: 'You Don\'t Know JS', price: '45' },
//   a1: undefined,
//   a2: null,
//   a3: 123
// }

第三步:循环引用

我们知道 JSON 无法深拷贝循环引用,遇到这种情况会抛出异常。

// 木易杨
// 此处 a 是文章开始的测试用例
a.circleRef = a;

JSON.parse(JSON.stringify(a));
// TypeError: Converting circular structure to JSON

1、使用哈希表

解决方案很简单,其实就是循环检测,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。

// 木易杨
function cloneDeep3(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
      
    var target = Array.isArray(source) ? [] : {};
    hash.set(source, target); // 新增代码,哈希表设值
    
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

测试一下,看看效果如何。

// 木易杨
// 此处 a 是文章开始的测试用例
a.circleRef = a;

var b = cloneDeep3(a);
console.log(b);
// {
//     name: "muyiy",
//     a1: undefined,
//    a2: null,
//     a3: 123,
//     book: {title: "You Don't Know JS", price: "45"},
//     circleRef: {name: "muyiy", book: {…}, a1: undefined, a2: null, a3: 123, …}
// }

完美!

2、使用数组

这里使用了 ES6 中的 WeakMap 来处理,那在 ES5 下应该如何处理呢?

也很简单,使用数组来处理就好啦,代码如下。

// 木易杨
function cloneDeep3(source, uniqueList) {

    if (!isObject(source)) return source; 
    if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
      
    var target = Array.isArray(source) ? [] : {};
    
    // ============= 新增代码
    // 数据已经存在,返回保存的数据
    var uniqueData = find(uniqueList, source);
    if (uniqueData) {
        return uniqueData.target;
    };
        
    // 数据不存在,保存源数据,以及对应的引用
    uniqueList.push({
        source: source,
        target: target
    });
    // =============

    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep3(source[key], uniqueList); // 新增代码,传入数组
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

// 新增方法,用于查找
function find(arr, item) {
    for(var i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }
    return null;
}

// 用上面测试用例已测试通过

现在已经很完美的解决了循环引用这种情况,那其实还是一种情况是引用丢失,我们看下面的例子。

// 木易杨
var obj1 = {};
var obj2 = {a: obj1, b: obj1};

obj2.a === obj2.b; 
// true

var obj3 = cloneDeep2(obj2);
obj3.a === obj3.b; 
// false

引用丢失在某些情况下是有问题的,比如上面的对象 obj2,obj2 的键值 a 和 b 同时引用了同一个对象 obj1,使用 cloneDeep2 进行深拷贝后就丢失了引用关系变成了两个不同的对象,那如何处理呢。

其实你有没有发现,我们的 cloneDeep3 已经解决了这个问题,因为只要存储已拷贝过的对象就可以了。

// 木易杨
var obj3 = cloneDeep3(obj2);
obj3.a === obj3.b; 
// true

完美!

第四步:拷贝 Symbol

这个时候可能要搞事情了,那我们能不能拷贝 Symol 类型呢?

当然可以,不过 SymbolES6 下才有,我们需要一些方法来检测出 Symble 类型。

方法一:Object.getOwnPropertySymbols(...)

方法二:Reflect.ownKeys(...)

对于方法一可以查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。(来自MDN)

var obj = {};
var a = Symbol("a"); // 创建新的symbol类型
var b = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol

obj[a] = "localSymbol";
obj[b] = "globalSymbol";

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols.length); // 2
console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
console.log(objectSymbols[0])      // Symbol(a)

对于方法二返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。(来自MDN)

Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]

var sym = Symbol.for("comet");
var sym2 = Symbol.for("meteor");
var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
           [sym2]: 0, "-1": 0, "8": 0, "second str": 0};
Reflect.ownKeys(obj);
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// 注意顺序
// Indexes in numeric order, 
// strings in insertion order, 
// symbols in insertion order

方法一

思路就是先查找有没有 Symbol 属性,如果查找到则先遍历处理 Symbol 情况,然后再处理正常情况,多出来的逻辑就是下面的新增代码。

// 木易杨
function cloneDeep4(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); 
      
    let target = Array.isArray(source) ? [] : {};
    hash.set(source, target);
    
    // ============= 新增代码
    let symKeys = Object.getOwnPropertySymbols(source); // 查找
    if (symKeys.length) { // 查找成功    
        symKeys.forEach(symKey => {
            if (isObject(source[symKey])) {
                target[symKey] = cloneDeep4(source[symKey], hash); 
            } else {
                target[symKey] = source[symKey];
            }    
        });
    }
    // =============
    
    for(let key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep4(source[key], hash); 
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

测试下效果

// 木易杨
// 此处 a 是文章开始的测试用例
var sym1 = Symbol("a"); // 创建新的symbol类型
var sym2 = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol

a[sym1] = "localSymbol";
a[sym2] = "globalSymbol";

var b = cloneDeep4(a);
console.log(b);
// {
//     name: "muyiy",
//     a1: undefined,
//    a2: null,
//     a3: 123,
//     book: {title: "You Don't Know JS", price: "45"},
//     circleRef: {name: "muyiy", book: {…}, a1: undefined, a2: null, a3: 123, …},
//  [Symbol(a)]: 'localSymbol',
//  [Symbol(b)]: 'globalSymbol'
// }

完美!

方法二

// 木易杨
function cloneDeep4(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); 
      
    let target = Array.isArray(source) ? [] : {};
    hash.set(source, target);
    
      Reflect.ownKeys(source).forEach(key => { // 改动
        if (isObject(source[key])) {
            target[key] = cloneDeep4(source[key], hash); 
        } else {
            target[key] = source[key];
        }  
      });
    return target;
}

// 测试已通过

这里使用了 Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可。

写到这里已经差不多了,我们再延伸下,对于 target 换一种写法,改动如下。

// 木易杨
function cloneDeep4(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); 
      
    let target = Array.isArray(source) ? [...source] : { ...source }; // 改动 1
    hash.set(source, target);
    
      Reflect.ownKeys(target).forEach(key => { // 改动 2
        if (isObject(source[key])) {
            target[key] = cloneDeep4(source[key], hash); 
        } else {
            target[key] = source[key];
        }  
      });
    return target;
}

// 测试已通过

在改动 1 中,返回一个新数组或者新对象,获取到源对象之后就可以如改动 2 所示传入 target 遍历赋值即可。

Reflect.ownKeys() 这种方式的问题在于不能深拷贝原型链上的数据,因为返回的是目标对象自身的属性键组成的数组。如果想深拷贝原型链上的数据怎么办,那用 for..in 就可以了。

我们再介绍下两个知识点,分别是构造字面量数组时使用展开语法构造字面量对象时使用展开语法。(以下代码示例来源于 MDN)

1、展开语法之字面量数组

这是 ES2015 (ES6) 才有的语法,可以通过字面量方式, 构造新数组,而不再需要组合使用 push, splice, concat 等方法。

var parts = ['shoulders', 'knees']; 
var lyrics = ['head', ...parts, 'and', 'toes']; 
// ["head", "shoulders", "knees", "and", "toes"]

这里的使用方法和参数列表的展开有点类似。

function myFunction(v, w, x, y, z) { }
var args = [0, 1];
myFunction(-1, ...args, 2, ...[3]);

返回的是新数组,对新数组修改之后不会影响到旧数组,类似于 arr.slice()

var arr = [1, 2, 3];
var arr2 = [...arr]; // like arr.slice()
arr2.push(4); 

// arr2 此时变成 [1, 2, 3, 4]
// arr 不受影响

展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(即只遍历一层)。

var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
// [[], [2], [3]]

这里 a 是多层数组,b 只拷贝了第一层,对于第二层依旧和 a 持有同一个地址,所以对 b 的修改会影响到 a。

2、展开语法之字面量对象

这是 ES2018 才有的语法,将已有对象的所有可枚举属性拷贝到新构造的对象中,类似于 Object.assign() 方法。

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// { foo: "baz", x: 42, y: 13 }

Object.assign() 函数会触发 setters,而展开语法不会。有时候不能替换或者模拟 Object.assign() 函数,因为会得到意想不到的结果,如下所示。

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
const merge = ( ...objects ) => ( { ...objects } );

var mergedObj = merge ( obj1, obj2);
// { 0: { foo: 'bar', x: 42 }, 1: { foo: 'baz', y: 13 } }

var mergedObj = merge ( {}, obj1, obj2);
// { 0: {}, 1: { foo: 'bar', x: 42 }, 2: { foo: 'baz', y: 13 } }

这里实际上是将多个解构变为剩余参数( rest ),然后再将剩余参数展开为字面量对象.

第五步:破解递归爆栈

上面四步使用的都是递归方法,但是有一个问题在于会爆栈,错误提示如下。

// RangeError: Maximum call stack size exceeded

那应该如何解决呢?其实我们使用循环就可以了,代码如下。

function cloneDeep5(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 广度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

由于篇幅问题就不过多介绍了,详情请参考下面这篇文章。

深拷贝的终极探索(99%的人都不知道)

本期思考题

如何用 JS 实现 JSON.parse?

参考

深入剖析 JavaScript 的深复制

深拷贝的终极探索(99%的人都不知道)

深入 js 深拷贝对象

MDN 之展开语法

MDN 之 Symbol

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,公众号「高级前端进阶」作者,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

查看原文

赞 2 收藏 2 评论 0

木易杨 发布了文章 · 2019-06-12

【进阶4-2期】Object.assign 原理及其实现

引言

上篇文章介绍了赋值、浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案。

本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带你手动实现一个浅拷贝,并在文末留下一道面试题,期待你的评论。

浅拷贝 Object.assign

上篇文章介绍了其定义和使用,主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象。(来自 MDN)

语法如下所示:

Object.assign(target, ...sources)

其中 target 是目标对象,sources 是源对象,可以有多个,返回修改后的目标对象 target

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

示例1

我们知道浅拷贝就是拷贝第一层的基本类型值,以及第一层的引用类型地址

// 木易杨
// 第一步
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "muyiy",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// } 
console.log(a === c);
// true

// 第二步
b.name = "change";
b.book.price = "55";
console.log(b);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

// 第三步
console.log(a);
// {
//     name: "muyiy",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "55"}
// } 

1、在第一步中,使用 Object.assign 把源对象 b 的值复制到目标对象 a 中,这里把返回值定义为对象 c,可以看出 b 会替换掉 a 中具有相同键的值,即如果目标对象(a)中的属性具有相同的键,则属性将被源对象(b)中的属性覆盖。这里需要注意下,返回对象 c 就是 目标对象 a。

2、在第二步中,修改源对象 b 的基本类型值(name)和引用类型值(book)。

3、在第三步中,浅拷贝之后目标对象 a 的基本类型值没有改变,但是引用类型值发生了改变,因为 Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用地址

示例2

String 类型和 Symbol 类型的属性都会被拷贝,而且不会跳过那些值为 nullundefined 的源对象。

// 木易杨
// 第一步
let a = {
    name: "muyiy",
    age: 18
}
let b = {
    b1: Symbol("muyiy"),
    b2: null,
    b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "muyiy",
//  age: 18,
//     b1: Symbol(muyiy),
//     b2: null,
//     b3: undefined
// } 
console.log(a === c);
// true

Object.assign 模拟实现

实现一个 Object.assign 大致思路如下:

1、判断原生 Object 是否支持该函数,如果不存在的话创建一个函数 assign,并使用 Object.defineProperty 将该函数绑定到 Object 上。

2、判断参数是否正确(目标对象不能为空,我们可以直接设置{}传递进去,但必须设置值)。

3、使用 Object() 转成对象,并保存为 to,最后返回这个对象 to。

4、使用 for..in 循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。

实现代码如下,这里为了验证方便,使用 assign2 代替 assign。注意此模拟实现不支持 symbol 属性,因为ES5 中根本没有 symbol

// 木易杨
if (typeof Object.assign2 != 'function') {
  // Attention 1
  Object.defineProperty(Object, "assign2", {
    value: function (target) {
      'use strict';
      if (target == null) { // Attention 2
        throw new TypeError('Cannot convert undefined or null to object');
      }

      // Attention 3
      var to = Object(target);
        
      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) {  // Attention 2
          // Attention 4
          for (var nextKey in nextSource) {
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

测试一下

// 木易杨
// 测试用例
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let c = Object.assign2(a, b);
console.log(c);
// {
//     name: "muyiy",
//  age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// } 
console.log(a === c);
// true

针对上面的代码做如下扩展。

注意1:可枚举性

原生情况下挂载在 Object 上的属性是不可枚举的,但是直接在 Object 上挂载属性 a 之后是可枚举的,所以这里必须使用 Object.defineProperty,并设置 enumerable: false 以及 writable: true, configurable: true

// 木易杨
for(var i in Object) {
    console.log(Object[i]);
}
// 无输出

Object.keys( Object );
// []

上面代码说明原生 Object 上的属性不可枚举。

我们可以使用 2 种方法查看 Object.assign 是否可枚举,使用 Object.getOwnPropertyDescriptor 或者 Object.propertyIsEnumerable 都可以,其中propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable: true。具体用法如下:

// 木易杨
// 方法1
Object.getOwnPropertyDescriptor(Object, "assign");
// {
//     value: ƒ, 
//  writable: true,     // 可写
//  enumerable: false,  // 不可枚举,注意这里是 false
//  configurable: true    // 可配置
// }

// 方法2
Object.propertyIsEnumerable("assign");
// false

上面代码说明 Object.assign 是不可枚举的。

介绍这么多是因为直接在 Object 上挂载属性 a 之后是可枚举的,我们来看如下代码。

// 木易杨
Object.a = function () {
    console.log("log a");
}

Object.getOwnPropertyDescriptor(Object, "a");
// {
//     value: ƒ, 
//  writable: true, 
//  enumerable: true,  // 注意这里是 true
//  configurable: true
// }

Object.propertyIsEnumerable("a");
// true

所以要实现 Object.assign 必须使用 Object.defineProperty,并设置 writable: true, enumerable: false, configurable: true,当然默认情况下不设置就是 false

// 木易杨
Object.defineProperty(Object, "b", {
    value: function() {
        console.log("log b");
    }
});

Object.getOwnPropertyDescriptor(Object, "b");
// {
//     value: ƒ, 
//  writable: false,     // 注意这里是 false
//  enumerable: false,  // 注意这里是 false
//  configurable: false    // 注意这里是 false
// }

所以具体到本次模拟实现中,相关代码如下。

// 木易杨
// 判断原生 Object 中是否存在函数 assign2
if (typeof Object.assign2 != 'function') {
  // 使用属性描述符定义新属性 assign2
  Object.defineProperty(Object, "assign2", {
    value: function (target) { 
      ...
    },
    // 默认值是 false,即 enumerable: false
    writable: true,
    configurable: true
  });
}

注意2:判断参数是否正确

有些文章判断参数是否正确是这样的。

// 木易杨
if (target === undefined || target === null) {
    throw new TypeError('Cannot convert undefined or null to object');
}

这样肯定没问题,但是这样写没有必要,因为 undefinednull 是相等的(高程 3 P52 ),即 undefined == null 返回 true,只需要按照如下方式判断就好了。

// 木易杨
if (target == null) { // TypeError if undefined or null
    throw new TypeError('Cannot convert undefined or null to object');
}

注意3:原始类型被包装为对象

// 木易杨
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); 
// { "0": "a", "1": "b", "2": "c" }

上面代码中的源对象 v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性

// 木易杨
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;

// Object.keys(..) 返回一个数组,包含所有可枚举属性
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.keys( v1 ); // [ '0', '1', '2' ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 ); 
// TypeError: Cannot convert undefined or null to object

// Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.getOwnPropertyNames( v1 ); // [ '0', '1', '2', 'length' ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 ); 
// TypeError: Cannot convert undefined or null to object

但是下面的代码是可以执行的。

// 木易杨
var a = "abc";
var b = {
    v1: "def",
    v2: true,
    v3: 10,
    v4: Symbol("foo"),
    v5: null,
    v6: undefined
}

var obj = Object.assign(a, b); 
console.log(obj);
// { 
//   [String: 'abc']
//   v1: 'def',
//   v2: true,
//   v3: 10,
//   v4: Symbol(foo),
//   v5: null,
//   v6: undefined 
// }

原因很简单,因为此时 undefinedtrue 等不是作为对象,而是作为对象 b 的属性值,对象 b 是可枚举的。

// 木易杨
// 接上面的代码
Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]

这里其实又可以看出一个问题来,那就是目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: 'abc'],那模拟实现时应该如何处理呢?很简单,使用 Object(..) 就可以了。

// 木易杨
var a = "abc";
console.log( Object(a) );
// [String: 'abc']

到这里已经介绍很多知识了,让我们再来延伸一下,看看下面的代码能不能执行。

// 木易杨
var a = "abc";
var b = "def";
Object.assign(a, b); 

答案是否定的,会提示以下错误。

// 木易杨
TypeError: Cannot assign to read only property '0' of object '[object String]'

原因在于 Object("abc") 时,其属性描述符为不可写,即 writable: false

// 木易杨
var myObject = Object( "abc" );

Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]

Object.getOwnPropertyDescriptor(myObject, "0");
// { 
//   value: 'a',
//   writable: false, // 注意这里
//   enumerable: true,
//   configurable: false 
// }

同理,下面的代码也会报错。

// 木易杨
var a = "abc";
var b = {
  0: "d"
};
Object.assign(a, b); 
// TypeError: Cannot assign to read only property '0' of object '[object String]'

但是并不是说只要 writable: false 就会报错,看下面的代码。

// 木易杨
var myObject = Object('abc'); 

Object.getOwnPropertyDescriptor(myObject, '0');
// { 
//   value: 'a',
//   writable: false, // 注意这里
//   enumerable: true,
//   configurable: false 
// }

myObject[0] = 'd';
// 'd'

myObject[0];
// 'a'

这里并没有报错,原因在于 JS 对于不可写的属性值的修改静默失败(silently failed),在严格模式下才会提示错误。

// 木易杨
'use strict'
var myObject = Object('abc'); 

myObject[0] = 'd';
// TypeError: Cannot assign to read only property '0' of object '[object String]'

所以我们在模拟实现 Object.assign 时需要使用严格模式。

注意4:存在性

如何在不访问属性值的情况下判断对象中是否存在某个属性呢,看下面的代码。

// 木易杨
var anotherObject = {
    a: 1
};

// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.b = 2;

("a" in myObject); // true
("b" in myObject); // true

myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true

这边使用了 in 操作符和 hasOwnProperty 方法,区别如下(你不知道的JS上卷 P119):

1、in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。

2、hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 原型链。

Object.assign 方法肯定不会拷贝原型链上的属性,所以模拟实现时需要用 hasOwnProperty(..) 判断处理下,但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败。

// 木易杨
var myObject = Object.create( null );
myObject.b = 2;

("b" in myObject); 
// true

myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function

解决方法也很简单,使用我们在【进阶3-3期】中介绍的 call 就可以了,使用如下。

// 木易杨
var myObject = Object.create( null );
myObject.b = 2;

Object.prototype.hasOwnProperty.call(myObject, "b");
// true

所以具体到本次模拟实现中,相关代码如下。

// 木易杨
// 使用 for..in 遍历对象 nextSource 获取属性值
// 此处会同时检查其原型链上的属性
for (var nextKey in nextSource) {
    // 使用 hasOwnProperty 判断对象 nextSource 中是否存在属性 nextKey
    // 过滤其原型链上的属性
    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
        // 赋值给对象 to,并在遍历结束后返回对象 to
        to[nextKey] = nextSource[nextKey];
    }
}

本期思考题

如何实现一个深拷贝?

参考

MDN 之 Object.assign()

ES2015系列(二) 理解 Object.assign

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

查看原文

赞 2 收藏 1 评论 0

木易杨 发布了文章 · 2019-05-29

【进阶 6-3 期】深入浅出节流函数 throttle

引言

上一节我们详细聊了聊高阶函数之柯里化,通过介绍其定义和三种柯里化应用,并在最后实现了一个通用的 currying 函数。这一小节会继续之前的篇幅聊聊函数节流 throttle,给出这种高阶函数的定义、实现原理以及在 underscore 中的实现,欢迎大家拍砖。

有什么想法或者意见都可以在评论区留言,下图是本文的思维导图,高清思维导图和更多文章请看我的 Github

66666333

定义及解读

函数节流指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。3 秒间隔结束后第一次遇到新的函数调用会触发执行,然后在这新的 3 秒内依旧无视后来产生的函数调用请求,以此类推。

img

举一个小例子,不知道大家小时候有没有养过小金鱼啥的,养金鱼肯定少不了接水,刚开始接水时管道中水流很大,水到半满时开始拧紧水龙头,减少水流的速度变成 3 秒一滴,通过滴水给小金鱼增加氧气。

此时「管道中的水」就是我们频繁操作事件而不断涌入的回调任务,它需要接受「水龙头」安排;「水龙头」就是节流阀,控制水的流速,过滤无效的回调任务;「滴水」就是每隔一段时间执行一次函数,「3 秒」就是间隔时间,它是「水龙头」决定「滴水」的依据。

如果你还无法理解,看下面这张图就清晰多了,另外点击 这个页面 查看节流和防抖的可视化比较。其中 Regular 是不做任何处理的情况,throttle 是函数节流之后的结果,debounce 是函数防抖之后的结果(下一小节介绍)。

image-20190525193539745

原理及实现

函数节流非常适用于函数被频繁调用的场景,例如:window.onresize() 事件、mousemove 事件、上传进度等情况。使用 throttle API 很简单,那应该如何实现 throttle 这个函数呢?

实现方案有以下两种

  • 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。
  • 第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。

这里我们采用第一种方案来实现,通过闭包保存一个 previous 变量,每次触发 throttle 函数时判断当前时间和 previous 的时间差,如果这段时间差小于等待时间,那就忽略本次事件触发。如果大于等待时间就把 previous 设置为当前时间并执行函数 fn。

我们来一步步实现,首先实现用闭包保存 previous 变量。

const throttle = (fn, wait) => {
    // 上一次执行该函数的时间
  let previous = 0
  return function(...args) {
    console.log(previous)
    ...
  }
}

执行 throttle 函数后会返回一个新的 function,我们命名为 betterFn。

const betterFn = function(...args) {
  console.log(previous)
    ...
}

betterFn 函数中可以获取到 previous 变量值也可以修改,在回调监听或事件触发时就会执行 betterFn,即 betterFn(),所以在这个新函数内判断当前时间和 previous 的时间差即可。

const betterFn = function(...args) {
  let now = +new Date();
  if (now - previous > wait) {
    previous = now
    // 执行 fn 函数
    fn.apply(this, args)
  }
}

结合上面两段代码就实现了节流函数,所以完整的实现如下。

// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
  // 上一次执行 fn 的时间
  let previous = 0
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date()
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

underscore 源码解读

上述代码实现了一个简单的节流函数,不过 underscore 实现了更高级的功能,即新增了两个功能

  • 配置是否需要响应事件刚开始的那次回调( leading 参数,false 时忽略)
  • 配置是否需要响应事件结束后的那次回调( trailing 参数,false 时忽略)

配置 { leading: false } 时,事件刚开始的那次回调不执行;配置 { trailing: false } 时,事件结束后的那次回调不执行,不过需要注意的是,这两者不能同时配置。

所以在 underscore 中的节流函数有 3 种调用方式,默认的(有头有尾),设置 { leading: false } 的,以及设置 { trailing: false } 的。上面说过实现 throttle 的方案有 2 种,一种是通过时间戳判断,另一种是通过定时器创建和销毁来控制。

第一种方案实现这 3 种调用方式存在一个问题,即事件停止触发时无法响应回调,所以 { trailing: true } 时无法生效。

第二种方案来实现也存在一个问题,因为定时器是延迟执行的,所以事件停止触发时必然会响应回调,所以 { trailing: false } 时无法生效。

underscore 采用的方案是两种方案搭配使用来实现这个功能。

const throttle = function(func, wait, options) {
  var timeout, context, args, result;
  
  // 上一次执行回调的时间戳
  var previous = 0;
  
  // 无传入参数时,初始化 options 为空对象
  if (!options) options = {};

  var later = function() {
    // 当设置 { leading: false } 时
    // 每次触发回调函数后设置 previous 为 0
    // 不然为当前时间
    previous = options.leading === false ? 0 : _.now();
    
    // 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
    timeout = null;
    
    // 执行函数
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  // 每次触发事件回调都执行这个函数
  // 函数内判断是否执行 func
  // func 才是我们业务层代码想要执行的函数
  var throttled = function() {
    
    // 记录当前时间
    var now = _.now();
    
    // 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
    // 并且设置了 { leading: false }(表示第一次回调不执行)
    // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
    if (!previous && options.leading === false) previous = now;
    
    // 距离下次触发 func 还需要等待的时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    
    // 要么是到了间隔时间了,随即触发方法(remaining <= 0)
    // 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
    // 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
    // 之后便会把 previous 值迅速置为 now
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        
        // clearTimeout(timeout) 并不会把 timeout 设为 null
        // 手动设置,便于后续判断
        timeout = null;
      }
      
      // 设置 previous 为当前时间
      previous = now;
      
      // 执行 func 函数
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 最后一次需要触发的情况
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
      // 间隔 remaining milliseconds 后触发 later 方法
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  // 手动取消
  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  // 执行 _.throttle 返回 throttled 函数
  return throttled;
};

小结

  • 函数节流指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求
  • 节流可以理解为养金鱼时拧紧水龙头放水,3 秒一滴

    • 「管道中的水」就是我们频繁操作事件而不断涌入的回调任务,它需要接受「水龙头」安排
    • 「水龙头」就是节流阀,控制水的流速,过滤无效的回调任务
    • 「滴水」就是每隔一段时间执行一次函数
    • 「3 秒」就是间隔时间,它是「水龙头」决定「滴水」的依据
  • 节流实现方案有 2 种

    • 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。
    • 第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。

参考

underscore.js

前端性能优化原理与实践

underscore 函数节流的实现

文章穿梭机

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-
  2. 关注我的 GitHub,让我们成为长期关系
  3. 关注公众号「高级前端进阶」,每周重点攻克一个前端面试重难点,公众号后台回复「资料」 送你精选前端优质资料。

查看原文

赞 0 收藏 0 评论 0

木易杨 赞了文章 · 2019-01-07

深拷贝的终极探索(99%的人都不知道)

划重点,这是一道面试必考题,我靠这道题刷掉了多少面试者✧(≖ ◡ ≖✿)嘿嘿

首先这是一道非常棒的面试题,可以考察面试者的很多方面,比如基本功,代码能力,逻辑能力,而且进可攻,退可守,针对不同级别的人可以考察不同难度,比如漂亮妹子就出1☆题,要是个帅哥那就得上5☆了,(*^__^*) 嘻嘻……

无论面试者多么优秀,漂亮的回答出问题,我总能够潇洒的再抛出一个问题,看着面试者露出惊异的眼神,默默一转身,深藏功与名

本文我将给大家破解深拷贝的谜题,由浅入深,环环相扣,总共涉及4种深拷贝方式,每种方式都有自己的特点和个性

深拷贝 VS 浅拷贝

再开始之前需要先给同学科普下什么是深拷贝,和深拷贝有关系的另个一术语是浅拷贝又是什么意思呢?如果对这部分部分内容了解的同学可以跳过

其实深拷贝和浅拷贝都是针对的引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型赋值,则会进行地址的拷贝,最终两个变量指向同一份数据

// 基本类型
var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 ,a b指向不同的数据

// 引用类型指向同一份数据
var a = {c: 1};
var b = a;
a.c = 2;
console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据

对于引用类型,会导致a b指向同一份数据,此时如果对其中一个进行修改,就会影响到另外一个,有时候这可能不是我们想要的结果,如果对这种现象不清楚的话,还可能造成不必要的bug

那么如何切断a和b之间的关系呢,可以拷贝一份a的数据,根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝

var a1 = {b: {c: {}};

var a2 = shallowClone(a1); // 浅拷贝
a2.b.c === a1.b.c // true

var a3 = clone(a3); // 深拷贝
a3.b.c === a1.b.c // false

浅拷贝的实现非常简单,而且还有多种方法,其实就是遍历对象属性的问题,这里只给出一种,如果看不懂下面的方法,或对其他方法感兴趣,可以看我的这篇文章

function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }

    return target;
}

最简单的深拷贝

深拷贝的问题其实可以分解成两个问题,浅拷贝+递归,什么意思呢?假设我们有如下数据

var a1 = {b: {c: {d: 1}};

只需稍加改动上面浅拷贝的代码即可,注意区别

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 注意这里
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

大部分人都能写出上面的代码,但当我问上面的代码有什么问题吗?就很少有人答得上来了,聪明的你能找到问题吗?

其实上面的代码问题太多了,先来举几个例子吧

  • 没有对参数做检验
  • 判断是否对象的逻辑不够严谨
  • 没有考虑数组的兼容

(⊙o⊙),下面我们来看看各个问题的解决办法,首先我们需要抽象一个判断对象的方法,其实比较常用的判断对象的方法如下,其实下面的方法也有问题,但如果能够回答上来那就非常不错了,如果完美的解决办法感兴趣,不妨看看这里吧

function isObject(x) {
    return Object.prototype.toString.call(x) === '[object Object]';
}

函数需要校验参数,如果不是对象的话直接返回

function clone(source) {
    if (!isObject(source)) return source;

    // xxx
}

关于第三个问题,嗯,就留给大家自己思考吧,本文为了减轻大家的负担,就不考虑数组的情况了,其实ES6之后还要考虑set, map, weakset, weakmap,/(ㄒoㄒ)/~~

其实吧这三个都是小问题,其实递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出

下面的代码可以生成指定深度和每层广度的代码,这段代码我们后面还会再次用到

function createData(deep, breadth) {
    var data = {};
    var temp = data;

    for (var i = 0; i < deep; i++) {
        temp = temp['data'] = {};
        for (var j = 0; j < breadth; j++) {
            temp[j] = j;
        }
    }

    return data;
}

createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}

当clone层级很深的话就会栈溢出,但数据的广度不会造成溢出

clone(createData(1000)); // ok
clone(createData(10000)); // Maximum call stack size exceeded

clone(createData(10, 100000)); // ok 广度不会溢出

其实大部分情况下不会出现这么深层级的数据,但这种方式还有一个致命的问题,就是循环引用,举个例子

var a = {};
a.a = a;

clone(a) // Maximum call stack size exceeded 直接死循环了有没有,/(ㄒoㄒ)/~~

关于循环引用的问题解决思路有两种,一直是循环检测,一种是暴力破解,关于循环检测大家可以自己思考下;关于暴力破解我们会在下面的内容中详细讲解

一行代码的深拷贝

有些同学可能见过用系统自带的JSON来做深拷贝的例子,下面来看下代码实现

function cloneJSON(source) {
    return JSON.parse(JSON.stringify(source));
}

其实我第一次简单这个方法的时候,由衷的表示佩服,其实利用工具,达到目的,是非常聪明的做法

下面来测试下cloneJSON有没有溢出的问题,看起来cloneJSON内部也是使用递归的方式

cloneJSON(createData(10000)); // Maximum call stack size exceeded

既然是用了递归,那循环引用呢?并没有因为死循环而导致栈溢出啊,原来是JSON.stringify内部做了循环引用的检测,正是我们上面提到破解循环引用的第一种方法:循环检测

var a = {};
a.a = a;

cloneJSON(a) // Uncaught TypeError: Converting circular structure to JSON

破解递归爆栈

其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环,当我提出用循环来实现时,基本上90%的前端都是写不出来的代码的,这其实让我很震惊

举个例子,假设有如下的数据结构

var a = {
    a1: 1,
    a2: {
        b1: 1,
        b2: {
            c1: 1
        }
    }
}

这不就是一个树吗,其实只要把数据横过来看就非常明显了

    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1       

用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点

首先我们往栈里放入种子数据,key用来存储放哪一个父元素的那一个子元素拷贝对象

然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝

function cloneLoop(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

改用循环后,再也不会出现爆栈的问题了,但是对于循环引用依然无力应对

破解循环引用

有没有一种办法可以破解循环应用呢?别着急,我们先来看另一个问题,上面的三种方法都存在的一个问题就是引用丢失,这在某些情况下也许是不能接受的

举个例子,假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象,o(╯□╰)o

var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = clone(a);
c.a1 === c.a2 // false

如果我们发现个新对象就把这个对象和他的拷贝存下来,每次拷贝对象前,都先看一下这个对象是不是已经拷贝过了,如果拷贝过了,就不需要拷贝了,直接用原来的,这样我们就能够保留引用关系了,✧(≖ ◡ ≖✿)嘿嘿

但是代码怎么写呢,o(╯□╰)o,别急往下看,其实和循环的代码大体一样,不一样的地方我用// ==========标注出来了

引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

find是抽象的一个函数,其实就是遍历uniqueList

// 保持引用关系
function cloneForce(x) {
    // =============
    const uniqueList = []; // 用来去重
    // =============

    let root = {};

    // 循环数组
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }
        
        // =============
        // 数据已经存在
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            break; // 中断本次循环
        }

        // 数据不存在
        // 保存源数据,在拷贝数据中对应的引用
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============
    
        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}

下面来验证一下效果,amazing

var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = cloneForce(a);
c.a1 === c.a2 // true

接下来再说一下如何破解循环引用,等一下,上面的代码好像可以破解循环引用啊,赶紧验证一下

惊不惊喜,(*^__^*) 嘻嘻……

var a = {};
a.a = a;

cloneForce(a)

看起来完美的cloneForce是不是就没问题呢?cloneForce有两个问题

第一个问题,所谓成也萧何,败也萧何,如果保持引用不是你想要的,那就不能用cloneForce了;

第二个问题,cloneForce在对象数量很多时会出现很大的问题,如果数据量很大不适合使用cloneForce

性能对比

上边的内容还是有点难度,下面我们来点更有难度的,对比一下不同方法的性能

我们先来做实验,看数据,影响性能的原因有两个,一个是深度,一个是每层的广度,我们采用固定一个变量,只让一个变量变化的方式来测试性能

测试的方法是在指定的时间内,深拷贝执行的次数,次数越多,证明性能越好

下面的runTime是测试代码的核心片段,下面的例子中,我们可以测试在2秒内运行clone(createData(500, 1)的次数

function runTime(fn, time) {
    var stime = Date.now();
    var count = 0;
    while(Date.now() - stime < time) {
        fn();
        count++;
    }

    return count;
}

runTime(function () { clone(createData(500, 1)) }, 2000);

下面来做第一个测试,将广度固定在100,深度由小到大变化,记录1秒内执行的次数

深度clonecloneJSONcloneLoopcloneForce
500351212338372
1000174104175143
15001166711282
200092508869

将上面的数据做成表格可以发现,一些规律

  • 随着深度变小,相互之间的差异在变小
  • clone和cloneLoop的差别并不大
  • cloneLoop > cloneForce > cloneJSON

我们先来分析下各个方法的时间复杂度问题,各个方法要做的相同事情,这里就不计算,比如循环对象,判断是否为对象

  • clone时间 = 创建递归函数 + 每个对象处理时间
  • cloneJSON时间 = 循环检测 + 每个对象处理时间 * 2 (递归转字符串 + 递归解析)
  • cloneLoop时间 = 每个对象处理时间
  • cloneForce时间 = 判断对象是否缓存中 + 每个对象处理时间

cloneJSON的速度只有clone的50%,很容易理解,因为其会多进行一次递归时间

cloneForce由于要判断对象是否在缓存中,而导致速度变慢,我们来计算下判断逻辑的时间复杂度,假设对象的个数是n,则其时间复杂度为O(n2),对象的个数越多,cloneForce的速度会越慢

1 + 2 + 3 ... + n = n^2/2 - 1

关于clone和cloneLoop这里有一点问题,看起来实验结果和推理结果不一致,其中必有蹊跷

接下来做第二个测试,将深度固定在10000,广度固定为0,记录2秒内执行的次数

宽度clonecloneJSONcloneLoopcloneForce
013400327214292989

排除宽度的干扰,来看看深度对各个方法的影响

  • 随着对象的增多,cloneForce的性能低下凸显
  • cloneJSON的性能也大打折扣,这是因为循环检测占用了很多时间
  • cloneLoop的性能高于clone,可以看出递归新建函数的时间和循环对象比起来可以忽略不计

下面我们来测试一下cloneForce的性能极限,这次我们测试运行指定次数需要的时间

var data1 = createData(2000, 0);
var data2 = createData(4000, 0);
var data3 = createData(6000, 0);
var data4 = createData(8000, 0);
var data5 = createData(10000, 0);

cloneForce(data1)
cloneForce(data2)
cloneForce(data3)
cloneForce(data4)
cloneForce(data5)

通过测试发现,其时间成指数级增长,当对象个数大于万级别,就会有300ms以上的延迟

总结

尺有所短寸有所长,无关乎好坏优劣,其实每种方法都有自己的优缺点,和适用场景,人尽其才,物尽其用,方是真理

下面对各种方法进行对比,希望给大家提供一些帮助

clonecloneJSONcloneLoopcloneForce
难度☆☆☆☆☆☆☆☆☆
兼容性ie6ie8ie6ie6
循环引用一层不支持一层支持
栈溢出不会不会
保持引用
适合场景一般数据拷贝一般数据拷贝层级很多保持引用关系

本文的灵感都来自于@jsmini/clone,如果大家想使用文中的4种深拷贝方式,可以直接使用@jsmini/clone这个库

// npm install --save @jsmini/clone
import { clone, cloneJSON, cloneLoop, cloneForce } from '@jsmini/clone';

本文为了简单和易读,示例代码中忽略了一些边界情况,如果想学习生产中的代码,请阅读@jsmini/clone的源码

@jsmini/clone孵化于jsmini,jsmini致力于为大家提供一组小而美,无依赖的高质量库

jsmini的诞生离不开jslib-base,感谢jslib-base为jsmini提供了底层技术

感谢你阅读了本文,相信现在你能够驾驭任何深拷贝的问题了,如果有什么疑问,欢迎和我讨论

最后推荐下我的新书《React状态管理与同构实战》,深入解读前沿同构技术,感谢大家支持

京东:https://item.jd.com/12403508.html

当当:http://product.dangdang.com/25308679.html

最后最后招聘前端,后端,客户端啦!地点:北京+上海+成都,感兴趣的同学,可以把简历发到我的邮箱: yanhaijing@yeah.net

原文网址:http://yanhaijing.com/javascr...

查看原文

赞 441 收藏 319 评论 72

木易杨 赞了文章 · 2019-01-06

高效工作的三部曲

一、表扬与批评员工,让员工具有更强的自我驱动能力

团队管理中有很多问题,每个员工有每个员工的个性,俗话说:十个指头都有长短,何况人呼?在一个团队中,总有些员工做得好,有些员工做得不好,那么该怎么样表扬和批评呢?如果把握的不好,做得好的、做得不好的都得罪了,以后谁给你卖命呢?

如何去表扬员工

1. 不是表扬能力,而是表扬成长

很多领导在表扬人时,可能喜欢说:干得不错,很有能力,继续努力。或者许诺一些利益等等。诚然,也不能说错,只是这是一个快速变化的时代,每个人的能力是需要不断提升的,可能这个员工这几个月干得不错,不代表他能一直干得不错。所以,如果你只表扬这个员工能力,会让员工觉得“我的能力可能还可以,但还是不够出类拔萃”。

尤其是一些新员工,他们会觉得表现的机会本来就不是很多,你却只是表扬了这个员工的能力,会让他觉得不是很真诚,你应该这样说,从员工成长的角度来表扬,部门同事都说你比六个月前有了很大进步。用这样的表扬方式,员工会觉得领导很细心,注意到自己的每一步努力,心里也就觉得自己的工作得到你的认同,以后会更加努力的。

2.善于借助别人的嘴,去表扬你的团队

这一点也很简单,也容易做到,有时候,可能你自己不善于表扬,这个时候,你可以这样对一个员工说,上次我们和某某部门合作的不错,很好的完成了哪项任务,那边的部门负责人对你的工作很认可,工作能力很突出。

相比和直接的表扬而言,其实员工更愿意听到间接的表扬,因为,直接的表扬可能存在是客气的因素,而间接表扬更显得真实可信。

作为团队管理者,不仅自己要表扬下属,有时候,还要学会借助别人的力量,可能是同样的话,但表达的效果确实截然不同的

如何适当有效地批评员工,在一定程度上,能够让员工意识到自己的不足

1. 不能对员工表现出负能量

员工有错误,要及时批评,不要把这些错误积攒在一起说,很多领导会犯这个毛病,经常数落员工的不是,劈头盖脸就是一顿批评,员工往往是措手不及,可能很多都是很久以前的事,还在这里说。

其实,员工做错了,并不是不要领导指出,而是希望领导尽快说出自己的不足,而不是听你的宣泄和不满,宣泄和不满只会让员工不接受呢的批评,心里往往是抵触的。

员工做错了,作为管理者需要做的是及时的提醒,避免一些不必要的批评,没有人喜欢被批评的。

2.批评要有“批评的理由”

批评员工的时候,要针对具体的事情,要把员工做错的地方指出来,也就是说,你需要给一个合理的批评理由。

具体可以这样做,你需要告诉你的下属,你对他的未来是如何期望的?;其次你要让下属知道,他现在的现状离期望值有多大的差距;最后,你还需要让他知道,下一步该如何做?去缩小现在和期望值这个差距。

3.针对抗压能力差的人,要先表扬后批评

团队中,总会有一些很脆弱、很敏感的人,这些人一旦受到批评,就会影响他们的情绪,给工作带来负面的影响。

面对这些脆弱的人,如果是按照常规的方法去批评他们,他们就会失去工作的热情。在批评这类员工时,关键在于,在批评的时候,要加入一些可以提高他们自我认同感的内容。

我们可以这样说,你对自己的未来有什么期望,然后肯定他的长处(不要说这个员工一无是处,任何人都有长处的,如果真没有,你早就该裁掉的)。要让他意识到自己的现状和期望有差距的时候,鼓励他是有潜力的,如果努力是有机会达到的。

当然,你也要给予一些指导,帮助他如何缩小这个之间的差距。

职场中,大部分员工还是希望努力工作,在职场获得认同烦人,作为领导者,也要关心员工的状态,用正确的方法,错误的方法,只能让员工对公司、对领导失去信心,这样对公司、对员工都是不利的,管理者要学会表扬和批评人的方法,才能双赢。

二、学会做减法

人的天性是喜欢增加而不喜欢减少,喜欢获得而不喜欢舍弃,但是,很多时候减少和舍弃会让我们的工作做得更好,特别是作为一名管理者,在柔性管理中,懂到授权给下属。但由于这种做法有时违背人得天性,因此很多人做不到,当然,这也就给了那些做的得到的人更多的机会了。

所谓减法,并不是不去做事,而是知道如何把最重要的时间用在最值得的事情上。

工作中善于做减法,会更高效

高昂的房价压在现代都市中的人身上,使得我们不得不进入快节奏的生活中,不停地去追求更多的时间。

我们每天要处理各种各样的事情,工作、生活、社交……事情繁杂,纵使有千头万绪,却也无处下手。

我们总想拥有更多的时间,总想追求更多的价值,而我们身心却逐渐进入了一种焦虑的状态。我们像背着沉重的负担,无法自如地前行。

这个时候的你,更应该在工作中学会做减法。

一支笔,一张纸,给自己5分钟。

整理自己思绪的过程也是将自己大脑清空的过程,放下那让你忐忑不安的事情,放下那让你惶恐的项目,放下让你不安的未来。就这一秒,什么都不用想,清空自己的脑袋。

然后,给自己5分钟,写下令人不安的事情。给自己做完减法,清空脑袋之后,你会发现自己的心里像卸下了一箱沉重的货物。

三、拒绝伪工作者

效率的高低不取决于开始了多少工作,而在于完成了多少。很多我们看上去非做不可的事,其实想通了并没有那么重要,因为它们是伪工作,所以,对于工作,提高效率都需要从拒绝伪工作开始。

每天的事情太多,总是做不完

对于咱们现代人来说,在工作中遇到的一个普遍的问题大概就是“每天的事情太多,总是做不完”,尤其是在那些发展较快的行业里,比如IT行业、媒体行业和金融行业的人对这个问题的体会会更加的深刻。

其实,一个人的效率是很难去提高的,自己唯一能够控制的就是少做一点事情,一些无关紧要的事情可选择不去做,而不是挤压时间把所有的事情凑合着去做完。而是应该善于去找到最重要的工作,并且优先去完成它们。

对于管理者而言,应该怎样去防止员工做伪工作,成为伪工作者呢?

首先,作为一个管理者,应该要让员工站在"做什么事情能够让公司最大获益"的高度去工作。这样,管理者才能够在做不完的工作中动脑筋去寻找那些对公司最有帮助的事情去做,而不是简单的应付老板派发下来的任务,然后像老板交差。

其次,管理者需要让员工明白,他们积极工作(而不是消极地完成任务),最大的受益者是自己。每个公司难免有员工对自己、环境、周围人的态度、所给予的机会等有不满的情绪,此时,这样的员工会消极地对待工作,不自己地成为不动脑筋的伪工作者,工作量看似很满,但是对于很多重要的工作没有去做。

So,当感觉自己因为总有干不完的工作而焦虑,抱怨时,不妨先停一下,重新梳理自己手边的工作,主动站在对公司业务最大角度,站在能够提升自己能力的角度,把那些最重要的工作找出来并完成它们。

查看原文

赞 5 收藏 0 评论 0

木易杨 发布了文章 · 2019-01-05

【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别

一、赋值(Copy)

赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋,两个变量具有相同的引用,指向同一个对象,相互之间有影响

对基本类型进行赋值操作,两个变量互不影响。

// 木易杨
let a = "muyiy";
let b = a;
console.log(b);
// muyiy

a = "change";
console.log(a);
// change
console.log(b);
// muyiy

对引用类型进行赋操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = a;
console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

通常在开发中并不希望改变变量 a 之后会影响到变量 b,这时就需要用到浅拷贝和深拷贝。

二、浅拷贝(Shallow Copy)

1、什么是浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

上图中,SourceObject 是原对象,其中包含基本类型属性 field1 和引用类型属性 refObj。浅拷贝之后基本类型数据 field2filed1 是不同属性,互不影响。但引用类型 refObj 仍然是同一个,改变之后会对另一个对象产生影响。

简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址

2、浅拷贝使用场景

  • Object.assign()

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

有些文章说Object.assign() 是深拷贝,其实这是不正确的。

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = Object.assign({}, a);
console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

  • 展开语法 Spread
// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = {...a};
console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

通过代码可以看出实际效果和 Object.assign() 是一样的。

  • Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

// 木易杨
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [4, 3]]

可以看出,改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

三、深拷贝(Deep Copy)

1、什么是深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2、深拷贝使用场景

JSON.parse(JSON.stringify(object))

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//     name: "muyiy",
//     book: {title: "You Don't Know JS", price: "45"}
// } 

完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。

我们看下对数组深拷贝效果如何。

// 木易杨
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [2, 3]]

对数组深拷贝之后,改变原数组不会影响到拷贝之后的数组。

但是该方法有以下几个问题。

1、会忽略 undefined

2、会忽略 symbol

3、不能序列化函数

4、不能解决循环引用的对象

5、不能正确处理new Date()

6、不能处理正则

  • undefinedsymbol 和函数这三种情况,会直接忽略。
// 木易杨
let obj = {
    name: 'muyiy',
    a: undefined,
    b: Symbol('muyiy'),
    c: function() {}
}
console.log(obj);
// {
//     name: "muyiy", 
//     a: undefined, 
//  b: Symbol(muyiy), 
//  c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
  • 循环引用情况下,会报错。
// 木易杨
let obj = {
    a: 1,
    b: {
        c: 2,
           d: 3
    }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
  • new Date 情况下,转换结果不正确。
// 木易杨
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

解决方法转成字符串或者时间戳就好了。

// 木易杨
let date = (new Date()).valueOf();
// 1545620645915

JSON.stringify(date);
// "1545620673267"

JSON.parse(JSON.stringify(date));
// 1545620658688
  • 正则情况下,
// 木易杨
let obj = {
    name: "muyiy",
    a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}

PS:为什么会存在这些问题可以学习一下 JSON。

除了上面介绍的深拷贝方法,常用的还有jQuery.extend()lodash.cloneDeep(),后面文章会详细介绍源码实现,敬请期待!

四、总结

--和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变会使原数据一同改变改变会使原数据一同改变
深拷贝改变会使原数据一同改变改变会使原数据一同改变

参考

js 深拷贝 vs 浅拷贝

Java 深拷贝和浅拷贝

MDN 之 Object.assign()

MDN 之展开语法

MDN 之 Array.prototype.slice()

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

查看原文

赞 1 收藏 0 评论 0

木易杨 发布了文章 · 2019-01-05

【进阶3-5期】深度解析 new 原理及模拟实现

定义

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 ——(来自于MDN)

举个栗子

function Car(color) {
    this.color = color;
}
Car.prototype.start = function() {
    console.log(this.color + " car start");
}

var car = new Car("black");
car.color; // 访问构造函数里的属性
// black

car.start(); // 访问原型里的属性
// black car start

可以看出 new 创建的实例有以下 2 个特性

  • 1、访问到构造函数里的属性
  • 2、访问到原型里的属性

注意点

ES6新增 symbol 类型,不可以使用 new Symbol(),因为 symbol 是基本数据类型,每个从Symbol()返回的 symbol 值都是唯一的。

Number("123"); // 123
String(123); // "123"
Boolean(123); // true
Symbol(123); // Symbol(123)

new Number("123"); // Number {123}
new String(123); // String {"123"}
new Boolean(true); // Boolean {true}
new Symbol(123); // Symbol is not a constructor

模拟实现

当代码 new Foo(...) 执行时,会发生以下事情:

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。

模拟实现第一步

new 是关键词,不可以直接覆盖。这里使用 create 来模拟实现 new 的效果。

new 返回一个新对象,通过 obj.__proto__ = Con.prototype 继承构造函数的原型,同时通过 Con.apply(obj, arguments)调用父构造函数实现继承,获取构造函数上的属性(【进阶3-3期】)。

实现代码如下

// 第一版
function create() {
    // 创建一个空的对象
    var obj = new Object(),
    // 获得构造函数,arguments中去除第一个参数
    Con = [].shift.call(arguments);
    // 链接到原型,obj 可以访问到构造函数原型中的属性
    obj.__proto__ = Con.prototype;
    // 绑定 this 实现继承,obj 可以访问到构造函数中的属性
    Con.apply(obj, arguments);
    // 返回对象
    return obj;
};

测试一下

// 测试用例
function Car(color) {
    this.color = color;
}
Car.prototype.start = function() {
    console.log(this.color + " car start");
}

var car = create(Car, "black");
car.color;
// black

car.start();
// black car start

完美!

不熟悉 apply / call 的点击查看:【进阶3-3期】深度解析 call 和 apply 原理、使用场景及实现

不熟悉继承的点击查看:JavaScript常用八种继承方案

模拟实现第二步

上面的代码已经实现了 80%,现在继续优化。

构造函数返回值有如下三种情况:

  • 1、返回一个对象
  • 2、没有 return,即返回 undefined
  • 3、返回undefined 以外的基本类型

情况1:返回一个对象

function Car(color, name) {
    this.color = color;
    return {
        name: name
    }
}

var car = new Car("black", "BMW");
car.color;
// undefined

car.name;
// "BMW"

实例 car 中只能访问到返回对象中的属性

情况2:没有 return,即返回 undefined

function Car(color, name) {
    this.color = color;
}

var car = new Car("black", "BMW");
car.color;
// black

car.name;
// undefined

实例 car 中只能访问到构造函数中的属性,和情况1完全相反。

情况3:返回undefined 以外的基本类型

function Car(color, name) {
    this.color = color;
    return "new car";
}

var car = new Car("black", "BMW");
car.color;
// black

car.name;
// undefined

实例 car 中只能访问到构造函数中的属性,和情况1完全相反,结果相当于没有返回值。

所以需要判断下返回的值是不是一个对象,如果是对象则返回这个对象,不然返回新创建的 obj对象。

所以实现代码如下:

// 第二版
function create() {
    // 创建一个空的对象
    var obj = new Object(),
    // 获得构造函数,arguments中去除第一个参数
    Con = [].shift.call(arguments);
    // 链接到原型,obj 可以访问到构造函数原型中的属性
    obj.__proto__ = Con.prototype;
    // 绑定 this 实现继承,obj 可以访问到构造函数中的属性
    var ret = Con.apply(obj, arguments);
    // 优先返回构造函数返回的对象
    return ret instanceof Object ? ret : obj;
};

【进阶3-4期】思考题解

问题:用 JS 实现一个无限累加的函数 add,示例如下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

// 以此类推

实现:

function add(a) {
    function sum(b) { // 使用闭包
        a = a + b; // 累加
        return sum;
     }
     sum.toString = function() { // 重写toString()方法
        return a;
    }
     return sum; // 返回一个函数
}

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

我们知道打印函数时会自动调用 toString()方法,函数 add(a) 返回一个闭包 sum(b),函数 sum() 中累加计算 a = a + b,只需要重写sum.toString()方法返回变量 a 就OK了。

参考

JavaScript 深入之 new 的模拟实现

MDN 之 new 运算符

MDN 之 Symbol

javascript 函数 add(1)(2)(3)(4) 实现无限极累加

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

查看原文

赞 2 收藏 2 评论 0

木易杨 发布了文章 · 2019-01-05

【进阶3-4期】深度解析bind原理、使用场景及模拟实现

bind()

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind返回的绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。(来自参考1)

语法:fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出bind 有如下特性:

  • 1、可以指定this
  • 2、返回一个函数
  • 3、可以传入参数
  • 4、柯里化

使用场景

1、业务场景

经常有如下的业务场景

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty

这里输出的nickname是全局的,并不是我们创建 person 时传入的参数,因为 setTimeout 在全局环境中执行(不理解的查看【进阶3-1期】),所以 this 指向的是window

这边把 setTimeout 换成异步回调也是一样的,比如接口请求回调。

解决方案有下面两种。

解决方案1:缓存 this

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
        var self = this; // added
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

解决方案2:使用 bind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

完美!

2、验证是否是数组

【进阶3-3期】介绍了 call 的使用场景,这里重新回顾下。

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 直接使用 toString()
[1, 2, 3].toString();     // "1,2,3"
"123".toString();         // "123"
123.toString();         // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"

可以通过toString() 来获取每个对象的类型,但是不同对象的 toString()有不同的实现,所以通过 Object.prototype.toString() 来检测,需要以 call() / apply() 的形式来调用,传递要检查的对象作为第一个参数。

另一个验证是否是数组的方法,这个方案的优点是可以直接使用改造后的 toStr

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 使用改造后的 toStr
toStr([1, 2, 3]);     // "[object Array]"
toStr("123");         // "[object String]"
toStr(123);         // "[object Number]"
toStr(Object(123)); // "[object Number]"

上面方法首先使用 Function.prototype.call函数指定一个 this 值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString 设置为传入参数。其实等价于 Object.prototype.toString.call()

这里有一个前提toString()方法没有被覆盖

Object.prototype.toString = function() {
    return '';
}
isArray([1, 2, 3]);
// false
3、柯里化(curry)
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

模拟实现

bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。

首先我们来实现以下四点特性:

  • 1、可以指定this
  • 2、返回一个函数
  • 3、可以传入参数
  • 4、柯里化
模拟实现第一步

对于第 1 点,使用 call / apply 指定 this

对于第 2 点,使用 return 返回一个函数。

结合前面 2 点,可以写出第一版,代码如下:

// 第一版
Function.prototype.bind2 = function(context) {
    var self = this; // this 指向调用者
    return function () { // 实现第 2点
        return self.apply(context); // 实现第 1 点
    }
}

测试一下

// 测试用例
var value = 2;
var foo = {
    value: 1
};

function bar() {
    return this.value;
}

var bindFoo = bar.bind2(foo);

bindFoo(); // 1
模拟实现第二步

对于第 3 点,使用 arguments 获取参数数组并作为 self.apply() 的第二个参数。

对于第 4 点,获取返回函数的参数,然后同第3点的参数合并成一个参数数组,并作为 self.apply() 的第二个参数。

// 第二版
Function.prototype.bind2 = function (context) {

    var self = this;
    // 实现第3点,因为第1个参数是指定的this,所以只截取第1个之后的参数
    // arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 

    return function () {
        // 实现第4点,这时的arguments是指bind返回的函数传入的参数
        // 即 return function 的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply( context, args.concat(bindArgs) );
    }
}

测试一下:

// 测试用例
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
};

var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}
模拟实现第三步

到现在已经完成大部分了,但是还有一个难点,bind 有以下一个特性

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

来个例子说明下:

var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

上面例子中,运行结果this.value 输出为 undefined,这不是全局value 也不是foo对象中的value,这说明 bindthis 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj。(【进阶3-1期】有介绍new的实现原理,下一期也会重点介绍)

这里可以通过修改返回函数的原型来实现,代码如下:

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 注释1
        return self.apply(
            this instanceof fBound ? this : context, 
            args.concat(bindArgs)
        );
    }
    // 注释2
    fBound.prototype = this.prototype;
    return fBound;
}
  • 注释1:

    • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
    • 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
  • 注释2: 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend

注意:这边涉及到了原型、原型链和继承的知识点,可以看下我之前的文章。

JavaScript常用八种继承方案

模拟实现第四步

上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改 this.prototype

来个代码测试下:

// 测试用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20

obj.habit; // 返回正确
// shopping

obj.friend; // 返回正确
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // 返回错误,这里被修改了
// Kitty

解决方案是用一个空对象作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承)。

var fNOP = function () {};            // 创建一个空对象
fNOP.prototype = this.prototype;     // 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP();        // 空对象的实例赋值给 fBound.prototype

这边可以直接使用ES5的 Object.create()方法生成一个新对象

fBound.prototype = Object.create(this.prototype);

不过 bindObject.create()都是ES5方法,部分IE浏览器(IE < 9)并不支持,Polyfill中不能用 Object.create()实现 bind,不过原理是一样的。

第四版目前OK啦,代码如下:

// 第四版,已通过测试用例
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(
            this instanceof fNOP ? this : context, 
            args.concat(bindArgs)
        );
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
模拟实现第五步

到这里其实已经差不多了,但有一个问题是调用 bind 的不是函数,这时候需要抛出异常。

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

所以完整版模拟实现代码如下:

// 第五版
Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

【进阶3-2期】思考题解

// 1、赋值语句是右执行的,此时会先执行右侧的对象
var obj = {
    // 2、say 是立即执行函数
    say: function() {
        function _say() {
            // 5、输出 window
            console.log(this);
        }
        // 3、编译阶段 obj 赋值为 undefined
        console.log(obj);
        // 4、obj是 undefined,bind 本身是 call实现,
        // 【进阶3-3期】:call 接收 undefined 会绑定到 window。
        return _say.bind(obj);
    }(),
};
obj.say();

【进阶3-3期】思考题解

call 的模拟实现如下,那有没有什么问题呢?

Function.prototype.call = function (context) {
    context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn;
    return result;
}

当然是有问题的,其实这里假设 context 对象本身没有 fn 属性,这样肯定不行,我们必须保证 fn属性的唯一性。

ES3下模拟实现

解决方法也很简单,首先判断 context中是否存在属性 fn,如果存在那就随机生成一个属性fnxx,然后循环查询 context 对象中是否存在属性 fnxx。如果不存在则返回最终值。

一种循环方案实现代码如下:

function fnFactory(context) {
    var unique_fn = "fn";
    while (context.hasOwnProperty(unique_fn)) {
        unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    
    return unique_fn;
}

一种递归方案实现代码如下:

function fnFactory(context) {
    var unique_fn = "fn" + Math.random();
    if(context.hasOwnProperty(unique_fn)) {
        // return arguments.callee(context); ES5 开始禁止使用
        return fnFactory(context); // 必须 return
    } else {
        return unique_fn;
    }
}

模拟实现完整代码如下:

function fnFactory(context) {
    var unique_fn = "fn";
    while (context.hasOwnProperty(unique_fn)) {
        unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    
    return unique_fn;
}

Function.prototype.call = function (context) {
    context = context || window;
    var fn = fnFactory(context); // added
    context[fn] = this; // changed

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context[fn](' + args +')'); // changed

    delete context[fn]; // changed
    return result;
}

// 测试用例在下面
ES6下模拟实现

ES6有一个新的基本类型Symbol,表示独一无二的值,用法如下。

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

不能使用 new 命令,因为这是基本类型的值,不然会报错。

new Symbol();
// TypeError: Symbol is not a constructor

模拟实现完整代码如下:

Function.prototype.call = function (context) {
  context = context || window;
  var fn = Symbol(); // added
  context[fn] = this; // changed

  let args = [...arguments].slice(1);
  let result = context[fn](...args); // changed

  delete context[fn]; // changed
  return result;
}
// 测试用例在下面

测试用例在这里:

// 测试用例
var value = 2;
var obj = {
    value: 1,
    fn: 123
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(null); 
// 2

console.log(bar.call(obj, 'kevin', 18));
// 1
// {value: 1, name: "kevin", age: 18}

console.log(obj);
// {value: 1, fn: 123}
扩展一下

有两种方案可以判断对象中是否存在某个属性。

var obj = {
     a: 2
};
Object.prototype.b = function() {
    return "hello b";
}
  • 1、in 操作符

in 操作符会检查属性是否存在对象及其 [[Prototype]] 原型链中。

("a" in obj);     // true
("b" in obj);     // true
  • 2、Object.hasOwnProperty(...)方法

hasOwnProperty(...)只会检查属性是否存在对象中,不会向上检查其原型链。

obj.hasOwnProperty("a");     //true
obj.hasOwnProperty("b");     //false

注意以下几点:

  • 1、看起来 in 操作符可以检查容器内是否有某个值,实际上检查的是某个属性名是否存在。对于数组来说,4 in [2, 4, 6] 结果返回 false,因为 [2, 4, 6] 这个数组中包含的属性名是0,1,2 ,没有4
  • 2、所有普通对象都可以通过 Object.prototype 的委托来访问 hasOwnProperty(...),但是对于一些特殊对象( Object.create(null) 创建)没有连接到 Object.prototype,这种情况必须使用 Object.prototype.hasOwnProperty.call(obj, "a"),显示绑定到 obj 上。又是一个 call 的用法

本期思考题

用 JS 实现一个无限累加的函数 add,示例如下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

// 以此类推

参考

不用 call 和 apply 方法模拟实现 ES5 的 bind 方法

JavaScript 深入之 bind 的模拟实现

MDN 之 Function.prototype.bind()

MDN 之 Symbol

第 4 章: 柯里化(curry)

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

查看原文

赞 2 收藏 2 评论 0

认证与成就

  • 获得 219 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

注册于 2018-10-31
个人主页被 1k 人浏览