前端面试基础知识整理

栏目: JavaScript · 发布时间: 5年前

内容简介:设置父元素高度;形成bfc;伪元素clear: both;引用:容器元素

一、CSS

1.css盒模型

  • w3c盒模型:content + padding + border + margin。元素宽高(css)等于content的宽高。
  • IE盒模型:元素的宽高(css)等于content + padding + border的宽度。

2.BFC块级格式化上下文

  • 形成条件:根元素;position: absolute/fixed; float;overflow:not visible;display: inline-block / table。
  • 应用:清除浮动

3.居中布局

  • 水平居中
    行内元素:text-align: center;块级元素:margin:auto;
  • 垂直居中
    line-height; absolute; flex;
  • 水平垂直居中
    absolute;伪元素形成行内元素居中;flex + justify-content + align-items

4.清除浮动

设置父元素高度;形成bfc;伪元素clear: both;

5.flex常用api

引用: Flex 布局教程:语法篇

容器元素 display: flex ;

容器属性

flex-direction // 决定主轴的方向(即项目的排列方向)。
flex-wrap // 如果一条轴线排不下,如何换行。
flex-flow // 是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
justify-content // 定义了项目在主轴上的对齐方式。
align-items // 属性定义项目在交叉轴上如何对齐。
align-content // 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

元素属性

order // 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
flex-grow // 属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
flex-shrink // 属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小
flex-basis // 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)
flex // flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto
align-self // align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。

二、JavaScript

1.原型与继承

原型:prototype

函数的对象属性,包含一个constructor属性,指向函数自身:Fun.prototype.constructor === Fun。

原型链

原型链由原型对象构成,是js对象继承的机制。每个对像都有[[prototype]]属性,主流浏览器以 __proto__ 暴露给用户。 __proto__ 指向对象的构造函数的原型属性(prototype)。

prototype的 __proto__ 又指向它的构造函数的原型;通过 __proto__ 属性,形成一个引用链。在访问对象的属性时,如果对象本身不存在,就会沿着这条[[prototype]]链式结构一层层的访问。

构造函数

可以通过new来新建一个对象的函数。

原型与继承

函数拥有原型( prototype );而对象可以通过原型链关系( [[prototype]] )实现继承。函数是一种特殊对象,也拥有 __proto__ ,即函数也会继承。函数的原型链是:函数静态方法—>原生函数类型的原型(拥有函数原生方法)—>原生对象原型(拥有对象原生方法) —> null

class A {}; class B extends A {};

前端面试基础知识整理

类的继承

B.__proto__ = A.prototype; B.prototype.__proto__ = A.prototype ;

new Fun操作符原理

  • Object.create(Fun.prototype);
  • 执行Fun,即类似类中的调用constructor()(为什么是执行constructor,因为constructor指向函数本身);
  • 如果constructor没有明确返回函数或对象,则返回第一步创建的对象。

前端面试基础知识整理

2.函数执行相关

堆内存与栈内存

  1. 栈内存:由编译器自动分配与释放。我们可以直接操作栈内存中的值。js的基本类型存放在栈内存中,按值引用;
  2. 堆内存:链表结构的类型,可以动态分配大小,js引用类型占用内存空间的大小不固定,存储在堆内存中。由于JS不允许直接访问堆内存中的位置,因此我们不能直接操作js的引用类型。而是生成一个指针,并将它放到栈内存中,通过这个指针来操作引用类型。

垃圾回收

类型:标记引用(无法解决循环引用问题);标记清除(现在主流回收算法)。

标记清除算法的核型概念是:从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

内存泄漏

对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

常见内存泄漏:

  1. 不合理的计时器(引用过时的外部变量)
  2. 被共享的闭包作用域(函数内部的函数共享一个闭包环境)
  3. 脱离dom引用(dom中元素被清除,但是变量还保持这对dom元素的引用)
  4. 意外的全局变量(未声明或者指向全局的this.xxx)

事件循环

事件循环是指: 执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表

microtask(jobs): promise / ajax / Object.observe(该方法已废弃)
macrotask(task): setTimout / script / IO / UI Rendering

每次事件循环:

  1. 执行完主执行线程中的任务。
  2. 取出micro-task中任务执行直到清空。
  3. 取出macro-task中一个任务执行。
  4. 取出micro-task中任务执行直到清空。
  5. 重复3和4。

前端面试基础知识整理

执行环境及作用域

执行环境

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境斗鱼一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的执行环境,在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法被创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。(全局执行环境直到应用程序推出时才会被销毁)。

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链,保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象,其在最开始的时候只包含一个变量:arguments。作用域链的下一个变量来自包含环境,而️再下一个变量对象则来自下一个包含环境一直延续到全局执行环境。作用域链在创建的时候就会被生成,保存在内部的[[scope]]属性当中。其本质是一个指向变量对象的指针列表,它指引用但不实际包含变量对象。

[[scope]]

函数创建过程中,会创建一个预先包含外部活动对象的作用域链,并始终包含全局环境的变量对象。这个作用域链被保存在内部的[[scope]]属性中,当函数执行时,会通过复制该属性中的对象构建起执行环境的作用域链。

执行上下文

定义

执行上下文是指当前Javascript代码被解析和执行时所在环境的抽象概念,JavaScript 任何代码的运行都是在执行上下文中。

类型

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval函数执行上下文

如何工作

执行上下文分为两个过程:

  1. 创建阶段
  2. 执行阶段

创建阶段

主要做了三件事

  1. this绑定(这解释了为什么this的值取决于函数的调用方式)
  2. 创建词法环境。(创建活动对象,变量和函数声明的位置标记(声明提升过程);根据[[scope]]创建作用域链)
  3. 创建变量环境(变量环境也是词法法环境,用于存储var声明的变量。)

执行阶段

变量赋值,语句执行等。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。其本质是函数的作用域链中保存着外部函数变量对象的引用。可以通过[[scope]]属性查看。因此,即使外部函数被销毁,但是由于外部环境中定义的变量在闭包的scope中还保持着引用,所以这些变量暂时不会被垃圾回收机制回收,因此依然可以在闭包函数中正常访问。 注意:同一个函数内部的闭包函数共享同一个外部闭包环境。 从下图可以看出,即使返回的闭包里面没有引用c,d变量,但是由于内部函数closure1中引用链,所以即使closure1未调用,闭包作用域中依然保存这些变量的引用。

前端面试基础知识整理

3.js引用方式

<script>
<script>
<script defer>
<script async>

前端面试基础知识整理

其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。

更多分析见下文 浏览器->页面的加载与渲染

4.对象的拷贝

浅拷贝

拷贝一层,内部如果是对象,则只是单纯的复制指针地址。

Object.assign()

深拷贝

  1. JSON.parse(JSON.stringify(source)),缺点层次太深会爆栈,无法解决循环引用问题。
  2. 参考 深拷贝的终极探索(99%的人都不知道) ,采用循环代替递归实现深层次的拷贝。大致思想如下:
// 保持引用关系
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;
}

5.模块化

  • es6模块
  • commonJS/AMD模块

es6模块与CommonJS模块的差异

  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口。

6.防抖和节流

防抖 (debounce)

将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可

function debounce(fn, wait, immediate) {
    let timer = null;

    return function(...args) {
        if (immediate && !timer) {
            fn.apply(this, args);
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, wait);
    };
}

节流(throttle)

每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。

function throttle(fn, wait, immediate) {
    let timer = null;
    let callNow = immediate;

    return function(...args) {
        if (callNow) {
            fn.apply(this, args);
            callNow = false;
        }

        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null;
            }, wait);
        }
    }
}

7.ES6/ES7

let/const

let和const可以形成块级作用域,并且不存在声明提升。并且在同一个块级作用域内不可重复声明。

解构赋值

比如 let [a, b, c] = [1, 2, 3]; let { foo } = { foo: 'bar' } ;

函数扩展

  1. 参数默认值
  2. rest参数
  3. 箭头函数

数组扩展

  1. Array.from() // 将类数组转化为数组
  2. Array.of() // 更语义明确的数组构建函数:用于将一组值,转换为数组。
  3. entries(),keys() 和 values()
  4. 实例方法includes
  5. flat(num?)// 数组扁平化
  6. 数组空位,es6明确将数组空位转化成undefined

对象扩展

  1. 属性简写
  2. 属性名表达式
  3. 可枚举性

    • for...in循环:只遍历对象自身的和继承的可枚举的属性。
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举
  4. Object.is()
  5. Object.assign()
  6. Object.keys(),Object.values(),Object.entries()

Symbol

let s = Symbol();

typeof s
// "symbol"

Symbol.for(), Symbol.keyFor()

Set,Map

Set

结构类似数组,成员唯一。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

操作方法:

  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

WeapSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set

其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map

类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

WeakMap

WeakMap与Map的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

Promise

实现一个简单的Promise

const FULLFILLED = 'FULLFILLED';
const REJECTED = 'REJECTED';
const PENDING = 'PENDING';
const isFn = val => typeof val === 'function';

class Promise {
    constructor(handler) {
        this._state = PENDING;
        this._value = null;
        this.FULLFILLED_CBS = [];
        this.REJECTED_CBS = [];

        try {
            handler(this._resolve.bind(this), this._reject.bind(this));
        } catch (err) {
            this._reject(err);
        }
    }

    _resolve(value) {
        const async = () => {
            if (this._state !== PENDING) {
                return;
            }
            this._state = FULLFILLED;
            
            const fullfilLed = (val) => {
                this._value = val;
                this.FULLFILLED_CBS.forEach(cb => cb(val));
            };
            const rejected = (err) => {
                this._value = err;
                this.REJECTED_CBS.forEach(cb => cb(err));
            };

            if (value instanceof Promise) {
                value.then(fullfilLed, rejected);
            } else {
                fullfilLed(value);
            }
        }

        requestAnimationFrame(async);
    }

    _reject(err) {
        const async = () => {
            if (this._state !== PENDING) {
                return;
            }
            this._state = REJECTED;
            this._value = err;
            this.REJECTED_CBS.forEach(cb => cb(err));
        }
        requestAnimationFrame(async);
    }

    then(onFullfilled, onRejected) {
        return new Promise((resolve, reject) => {
            const handerResolve = (value) => {
                try {
                    if (!isFn(onFullfilled)) {
                        resolve(value);
                    }
                    const res = onFullfilled(value);
    
                    if (res instanceof Promise) {
                        res.then(resolve, reject);
                    } else {
                        resolve(res);
                    }
                } catch (err) {
                    reject(err);
                }
            };
            const handlerReject = (err) => {
                try {
                    if (!isFn(onRejected)) {
                        reject(err);
                    }
                    const res  = onRejected(err);
                    if (res instanceof Promise) {
                        res.then(resolve, reject);
                    } else {
                        reject(res);
                    }
                } catch (err) {
                    reject(err);
                }
            };
            switch(this._state) {
                case PENDING:
                    this.FULLFILLED_CBS.push(handerResolve);
                    this.REJECTED_CBS.push(handlerReject);
                    break;
                case FULLFILLED:
                    handerResolve(this._value);
                    break;
                case REJECTED:
                    handlerReject(this._value);
                    break;
                default: break;
            }
        });
    }

    catch(onRejected) {
        return this.then(undefined, onRejected);
    }

    finally(cb) {
        const P = this.constructor;
        return this.then(
            /** 使用then保证promise的流是正常的,因为promise的下一步总是建立在上一步执行完的基础上 */
            value  => P.resolve(cb()).then(() => value),
            reason => P.resolve(cb()).then(() => { throw reason })
        );
    }

    static all(list) {
        return new Promise(resolve, reject) {
            let count = 0;
            const values = [];
            const P = this.constructor;

            for (let [key, fn] of list) {
                P.resolve(fn).then(res => {
                    values[key] = res;
                    count++;
                    if (count === list.length) {
                        resolve(values);
                    }
                }, err => reject(err));
            }
        }
    }

    static race(list) {
        return new Promise((resolve, reject) => {
            const values = [];
            let count = 0;
            const P = this.constructor;
            for (let fn of list) {
                P.resolve(fn).then(res => {
                    resolve(res);
                }, err => {
                    reject(err);
                })
            }
        })
    }

    static resolve(value) {
        if (value instanceof Promise) {
            return value;
        }

        return new Promise(resolve => resolve(value));
    }

    static reject(value) {
        return new Promise((resolve, reject) => reject(value));
    }
}

async/await

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async函数返回一个promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。

Class

es6的类语法是es5构造函数的语法糖,但是也有一些不同点

  1. class的原型属性不可枚举
  2. 不使用new操作符,class会报错
  3. class新增存取函数
  4. 不存在声明提升

Class的继承

抓住两点即可:假设A为父类,B为子类那么

B.__proto__ = A; // 子类的__proto__属性,表示构造函数的继承,总是指向父类。
B.prototype.__proto__ = A.prototype; // 子类prototype属性的__proto__属性,表示方法(实例)的继承,总是指向父类的prototype属性。

Module

  • es6模块
  • commonJS/AMD模块

es6模块与CommonJS模块的差异

  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口。

8.函数柯里化

在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

const add = function add(x) {
    return function (y) {
        return x + y
    }
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

9.正则表达式

3.浏览器

1.页面的加载与渲染

从输入 url 到展示的过程

  1. DNS查找相应的ip地址
    浏览器DNS缓存->电脑本地host文件查找->运营商DNS的解析
  2. 三次握手

    • 客户端发送 syn(同步序列编号) 请求,进入 syn_send 状态,等待确认
    • 服务端接收并确认 syn 包后发送 syn+ack 包,进入 syn_recv 状态
    • 客户端接收 syn+ack 包后,发送 ack 包,双方进入 established 状态
  3. 发送请求,分析 url,设置请求报文(头,主体)
  4. 服务器返回请求的文件 (html)
  5. 浏览器渲染

    • HTML parser --> DOM Tree
    • CSS parser --> Style Tree
    • attachment --> Render Tree
    • layout: 布局

浏览器加载与渲染

浏览器下载html文件并进行编译,转化成类似下面的结构

前端面试基础知识整理

图片来源 再谈 load 与 DOMContentLoaded 浏览器会对转化后的数据结构自上而下进行分析:首先开启下载线程,对所有的资源进行优先级 排序 下载(注意,这里仅仅是下载,区别于解析过程,解析过程可能被阻塞)。同时主线程会对文档进行解析

css文件的解析

  • header中的css的下载不会阻塞html的解析,当css文件下载完成,将阻塞html解析,优先解析css,会阻碍渲染。
  • body中的css下载和解析都不会阻碍html的解析,会阻碍渲染。(存在特殊情况,见下文)

js文件的解析

1. 普通的script:

遇到 script 标签时,首先阻塞后续内容的解析(但是不会阻塞下载,chrome对同一个域名支持6个http同时下载),当下载完成后,便执行js文件中的同步代码。然后接着解析html

2. defer和async

如果script标签设置了async或者defer标签,下载过程不会阻塞文档解析。defer执行将会放到html解析完成之后,dcl事件之前。async则是下载完就执行,并且阻塞解析。

body中的首个script/link

在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是link,表现有点奇怪,下载和解析会阻塞html的解析,并有可能进行首次渲染,也可能不渲染。

渲染过程大致为:计算样式 Recalculate style ->布局:layout->更新布局树update layer tree -> 绘制paint

3.相关概念

dom构建

DOM 构建的意思是,将文档中的所有 DOM 元素构建成一个树型结构。

css构建

将文档中的所有 css 资源合并。生成css rule tree

render 树

将 DOM 树和 CSS 合并成一棵渲染树,render 树在合适的时机会被渲染到页面中。(比如遇到 script 时, 该 script 还没有下载到本地时)。

4.总结

如下图:

  1. 浏览器首先下载该地址所对应的 html 页面。(如果html很大,浏览器并不会等待html完全下载完,而是采用分段下载,分段解析)
  2. 浏览器解析 html 页面的 DOM 结构。
  3. 开启下载线程对文档中的所有资源按优先级排序下载。
  4. 主线程继续解析文档,到达 head 节点 ,head 里的外部资源无非是外链样式表和外链 js。

    • 发现有外链 css 或者外链 js,如果是外链 js ,则停止解析后续内容,等待该资源下载,下载完后立刻执行。如果是外链 css,继续解析后续内容。
  5. 解析到 body

在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是link,表现有点奇怪,下载和解析会阻塞html的解析,并有可能进行首次渲染,也可能不渲染。随后js的下载和执行依然会阻塞解析,但是css的下载和解析则不会阻塞html的解析。

  1. 文档解析完毕,页面重新渲染。当页面引用的所有 js 同步代码执行完毕,触发 DOMContentLoaded 事件。然后执行布局:layout->更新布局树update layer tree -> 绘制paint
  2. html 文档中的图片资源,js 代码中有异步加载的 css、js 、图片资源都加载完毕之后,load 事件触发(与DOMContentLoaded没有必然的先后顺序关系)。

前端面试基础知识整理

前端面试基础知识整理

2.跨域解决方案

4.计算机网络基础

5.前端工程化

6.Vue源码相关

7.React源码相关

8.算法相关

引用参考


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从需求到产品:0岁产品经理进阶之道

从需求到产品:0岁产品经理进阶之道

权莉 / 人民邮电出版社 / 2018-7 / 49.80元

本书主要针对刚入职的初级产品经理,从贴近工作状态的场景切入,对各阶段的知识点进行分类总结,旨在提供一套经过实践检验的产品方法论,为读者从初级产品经理成长为产品经理奠定坚实的基础。 书中提炼的方法和案例涵盖初级产品经理工作的方方面面,从基本技能到思维方式,从需求管理到产品规划定义,从框架选型到流程梳理,从工作模块拆解到案例剖析,用具体且贴合实际工作场景的内容,还原真实的产品工作方法及实践案例,既有方......一起来看看 《从需求到产品:0岁产品经理进阶之道》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具