tryzf

tryzf 查看完整档案

杭州编辑浙江经济职业技术学院  |  信息技术学院 编辑杭州纳舍科技  |  前端开发 编辑 tryzf.online 编辑
编辑

小小小前端

个人动态

tryzf 赞了文章 · 2019-07-08

git commit 代码提交规范

git commit 代码提交规范

一、为什么需要制定提交规范?

在团队协作开发时,每个人提交代码时都会写 commit message。

每个人都有自己的书写风格,翻看我们组的git log, 可以说是五花八门,十分不利于阅读和维护。

一般来说,大厂都有一套的自己的提交规范,尤其是在一些大型开源项目中,commit message 都是十分一致的。

因此,我们需要制定统一标准,促使团队形成一致的代码提交风格,更好的提高工作效率,成为一名有追求的工程师。

二、业界通用的 git 提交规范有哪些?

1. commitizen

AngularJS 在 github上 的提交记录被业内许多人认可,逐渐被大家引用。

格式:

type(scope) : subject

( 1 ) type(必须) : commit 的类别,只允许使用下面几个标识:

  • feat : 新功能
  • fix : 修复bug
  • docs : 文档改变
  • style : 代码格式改变
  • refactor : 某个已有功能重构
  • perf : 性能优化
  • test : 增加测试
  • build : 改变了build工具 如 grunt换成了 npm
  • revert : 撤销上一次的 commit
  • chore : 构建过程或辅助工具的变动

( 2 ) scope(可选) : 用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

( 3 ) subject(必须) : commit 的简短描述,不超过50个字符。
commitizen 是一个撰写合格 Commit message 的工具,
遵循 Angular 的提交规范。

安装:

全局安装 commitizen

npm install -g commitizen

进入项目文件夹,运行如下命令:

commitizen init cz-conventional-changelog --save --save-exact

使用:

git cz 命令取代 git commit(先使用git add),这时会出现如下选项:

( 1 )选择 type

( 2 )填写 scope(选填)

? What is the scope of this change (e.g. component or file name)? (press enter to skip)
core

( 3 )填写 subject

? Write a short, imperative tense description of the change:
set a to b

完成,运行 git log 命令,查看我们刚才提交的 commit message,如下:

fix(core): set a to b

优点:

  • 符合业内标准(许多项目使用 AngularJS 的commit 规范)
  • 提交过程更加规范(使用 commitizen 规范工具,风格统一)
  • 能够生成风格统一的 commit log(type(scope):subject)

缺点:

  • 需要安装 commitizen 工具包,使项目更大、更重了(适合大型开源项目)
  • 提交过程受约束较大
  • 有一定的学习成本

2. 设置 git commit 模板

步骤如下:

( 1 ) 建立模板文件

在项目中建立 .git_template 文件,内容可以自定义:

type:
scope:
subject:

( 2 ) 设置模板

运行如下命令:

git config commit.template .git_template // 当前项目
<!-- git config commit.template .git_template // 全局设置 -->

( 3 ) 提交代码

先使用 git add 添加代码
使用 git commit 按照模板填写
最后 git push 推送到远端

优点:

  • 规则可配置,更自由
  • 配置方式简洁(只需添加配置文件)

缺点:

  • 便利性差,每次都要用 vim 编辑器填写模板
  • 易出错,没有可靠的校验方式

三、制定适合我们的 git commit 提交规范

第二章中提到的两种业内普遍使用的规范,都不完全适合我们。

第一种方式适合大型开源项目,我们如果也照搬会比较麻烦,但我们可以借鉴 type(scope): subject 的提交格式,也算是与大厂同步;
第二种方式虽然自由,但是也不比较麻烦,要配置模板。
因此,我们只模仿 type(scope): subject 的提交格式,不使用工具 or 模板校验,靠大家自觉遵守即可。

格式

type: description

1. type 类型

type 是 commit 的类别,只允许如下几种标识:

  • fix: 修复bug
  • add: 新功能
  • update: 更新
  • style : 代码格式改变
  • test: 增加测试代码
  • revert: 撤销上一次的commit
  • build: 构建工具或构建过程等的变动,如:gulp 换成了 webpack,webpack 升级等

2. description

description 是对本次提交的简短描述。

不超过50个字符。

推荐以动词开头,如: 设置、修改、增加、删减、撤销等

最后,既然制定了规则,大家就遵守起来吧~~~~

查看原文

赞 51 收藏 38 评论 2

tryzf 回答了问题 · 2019-05-31

小程序里怎么获取组件的DOM元素

clipboard.png

关注 4 回答 3

tryzf 赞了回答 · 2018-11-26

解决Laravel Nginx 除 `/` 外所有路由 404

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

详见Laravel官方文档:
http://laravel.com/docs/5.0/installation#pretty-urls

关注 13 回答 10

tryzf 回答了问题 · 2018-11-08

小程序textarea遮挡键盘

设置cursor-spacing, 文档属性说明:

指定光标与键盘的距离,单位px或rpx,默认为px。取 textarea 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离

关注 5 回答 3

tryzf 收藏了文章 · 2018-09-27

大厂的532道面试题知识点笔记

express&koa

面试题目:1.express和koa的对比,两者中间件的原理,koa捕获异常多种情况说一下

参考:https://blog.csdn.net/shmnh/a...
https://blog.csdn.net/K616358...
https://blog.csdn.net/wang839...
async 函数:http://www.ruanyifeng.com/blo...

初识两者

express:

var express = require('express')
var app = express()  //创建一个APP实例
 
//建一个项目根目录的get请求路由,回调方法中直接输出字符串Hello World!
app.get('/', function (req, res) {
    res.send('Hello World!')
});
 
//监听端口,启动服务
app.listen(3000);

koa:


var koa = require('koa');
var route = require('koa-route');  //koa默认没有集成route功能,引入中间件
 
var app = koa();  //创建一个APP实例
 
//建一个项目根目录的get请求路由,回调方法中直接输出字符串Hello World!,就是挂载一个中间件
app.use(route.get('/', function *(){
    this.body = 'Hello World';
}));
 
//监听端口,启动服务

app.listen(3000);

启动方式

koa采用了new Koa()的方式,而express采用传统的函数形式,对比源码如下:

//koa
const Emitter = require('events');
module.exports = class Application extends Emitter {
...
}
//express
exports = module.exports = createApplication;
function createApplication() {
...
}

应用生命周期和上下文

在项目过程中,经常需要用到在整个应用生命周期中共享的配置和数据对象,比如服务URL、是否启用某个功能特性、接口配置、当前登录用户数据等等。

express:


//共享配置,express提供了很多便利的方法
app.set('enableCache', true)
app.get('enableCache')//true
 
app.disable('cache')
app.disabled('cache')//true
 
app.enable('cache')
app.enabled('cache')//true
 
//应用共享数据:app.locals

app.locals.user = {name:"Samoay", id:1234};

koa:


//配置,直接使用koa context即可
app.enableCache = true;
 
app.use(function *(next){
    console.log(this.app.enableCache);
    //true
    this.app.enableCache = false;
 
    //just use this
    this.staticPath = 'static';
 
    yield *next;
});
 
//应用共享数据:ctx.state
this.state.user = {name:"Samoay", id:1234}; 

请求HTTP Request

服务器端需要进行什么处理,怎么处理以及处理的参数都依赖客户端发送的请求,两个框架都封装了HTTP Request对象,便于对这一部分进行处理。以下主要举例说明下对请求参数的处理。GET参数都可以直接通过Request对象获取,POST参数都需要引入中间件先parse,再取值。

express:


// 获取QueryString参数
// GET /shoes?order=desc&shoe[color]=blue
req.query.order
// => "desc"
 
req.query.shoe.color
// => "blue"
 
// 通过路由获取Restful风格的URL参数
app.get('/user/:id?', function userIdHandler(req, res) {
    console.log(req.params.id);
    res.send('GET');
})
 
//获取POST数据:需要body-parser中间件
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/', function (req, res) {
    console.log(req.body);
    res.json(req.body);

koa:


// 获取QueryString参数
// GET /?action=delete&id=1234
this.request.query
// => { action: 'delete', id: '1234' }
 
// 通过路由获取Restful风格的URL参数
var route = require('koa-route');
app.use(route.get('/post/:id', function *(id){
    console.log(id);
    // => 1234
}));
 
// 获取POST数据:需要co-body中间件
// Content-Type: application/x-www-form-urlencoded
// title=Test&content=This+is+a+test+post
var parse = require('co-body');
app.use(route.post('/post/new', function *(){
    var post = yield parse(this.request);//this
    console.log(post);
    // => { title: 'Test', content: 'This is a test post' }
}));

路由Route

收到客户端的请求,服务需要通过识别请求的方法(HTTP Method: GET, POST, PUT...)和请求的具体路径(path)来进行不同的处理。这部分功能就是路由(Route)需要做的事情,说白了就是请求的分发,分发到不同的回调方法去处理。

express

// app.all表示对所有的路径和请求方式都要经过这些回调方法的处理,可以逗号方式传入多个
app.all('*', authentication, loadUser);
// 也可以多次调用
app.all('*', requireAuthentication)
app.all('*', loadUser);
// 也可以针对某具体路径下面的所有请求
app.all('/api/*', requireAuthentication);
 
// app.get GET方式的请求
app.get('/user/:id', function(req, res) {
    res.send('user ' + req.params.id);
});
 
// app.post  POST方式的请求
app.post('/user/create', function(req, res) {
    res.send('create new user');
});

这里需要说明2个问题,首先是app.get,在应用生命周期中也有一个app.get方法,用于获取项目配置。Express内部就是公用的一个方法,如果传入的只有1个参数就获取配置,2个参数就作为路由处理。其次是app.use('', cb) 与app.all('', cb) 的区别,前者是中间件方式,调用是有顺序的,不一定会执行到;后者是路由方式,肯定会执行到。

koa

// Koa
// 和Express不同,koa需要先引入route中间件
var route = require('koa-route');
 
//引入中间件之后支持的写法差不多,只是路径传入route,然后把route作为中间件挂载到app
app.use(route.get('/', list));
app.use(route.get('/post/new', add));
app.use(route.get('/post/:id', show));
app.use(route.post('/post', create));
 
//链式写法
var router = require('koa-router')();
 
router.get('/', list)
      .get('/post/new', add)
      .get('/post/:id', show)
      .post('/post', create);
 
app.use(router.routes())
   .use(router.allowedMethods());

视图view

Express框架自身集成了视图功能,提供了consolidate.js功能,可以是有几乎所有Javascript模板引擎,并提供了视图设置的便利方法。Koa需要引入co-views中间件,co-views也是基于consolidate.js,支持能力一样强大。

express


// Express
// 这只模板路径和默认的模板后缀
app.set('views', __dirname + '/tpls');
app.set('view engine', 'html');
 
//默认,express根据template的后缀自动选择模板
//引擎渲染,支持jade和ejs。如果不使用默认扩展名
app.engine(ext, callback)
 
app.engine('html', require('ejs').renderFile);
 
//如果模板引擎不支持(path, options, callback)
var engines = require('consolidate');
app.engine('html', engines.handlebars);
app.engine('tpl', engines.underscore);
 
app.get('list', function(res, req){
    res.render('list', {data});
});

koa

//需要引入co-views中间件
var views = require('co-views');
 
var render = views('tpls', {
    map: { html: 'swig' },//html后缀使用引擎
    default: "jade"//render不提供后缀名时
});
 
var userInfo = {
    name: 'tobi',
    species: 'ferret'
};
 
var html;
html = render('user', { user: userInfo });
html = render('user.jade', { user: userInfo });
html = render('user.ejs', { user: userInfo });

返回HTTP Response

获取完请求参数、处理好了具体的请求、视图也准备就绪,下面就该返回给客户端了,那就是HTTP Response对象了。这部分也属于框架的基础部分,各种都做了封装实现,显著的区别是koa直接将输出绑定到了ctx.body属性上,另外输出JSON或JSONP需要引入中间件。

express


//输出普通的html
res.render('tplName', {data});
 
//输出JSON
res.jsonp({ user: 'Samoay' });
// => { "user": "Samoay" }
 
//输出JSONP   ?callback=foo
res.jsonp({ user: 'Samoay' });
// => foo({ "user": "Samoay" });
 
//res.send([body]);
res.send(new Buffer('whoop'));
res.send({ some: 'json' });
res.send('<p>some html</p>');
 
//设定HTTP Status状态码
res.status(200);

koa

app.use(route.get('/post/update/:id', function *(id){
    this.status = 404;
    this.body = 'Page Not Found';
}));
 
var views = require('co-views');
var render = views('tpls', {
    default: "jade"//render不提供后缀名时
});
app.use(route.get('/post/:id', function *(id){
    var post = getPost(id);
    this.status = 200;//by default, optional
    this.body = yield render('user', post);
}));
 
//JSON
var json = require('koa-json');
app.use(route.get('/post/:id', function *(id){
    this.body = {id:1234, title:"Test post", content:"..."};
}));

中间件 Middleware

对比了主要的几个框架功能方面的使用,其实区别最大,使用方式最不同的地方是在中间件的处理上。Express由于是在ES6特性之前的,中间件的基础原理还是callback方式的;而koa得益于generator特性和co框架(co会把所有generator的返回封装成为Promise对象),使得中间件的编写更加优雅。

express

// req 用于获取请求信息, ServerRequest 的实例
// res 用于响应处理结果, ServerResponse 的实例
// next() 函数用于将当前控制权转交给下一步处理,
//        如果给 next() 传递一个参数时,表示出错信息
var x = function (req, res, next) {
 
    // 对req和res进行必要的处理
 
    // 进入下一个中间件
    return next();
 
    // 传递错误信息到下一个中间件
    return next(err);
 
    // 直接输出,不再进入后面的中间件
    return res.send('show page');
};

koa


// koa 一切都在ctx对象上+generator
app.use(function *(){
    this; // is the Context
 
    this.request; // is a koa Request
    this.response; // is a koa Response
 
    this.req;// is node js request
    this.res;// is node js response
 
    //不再进入后面的中间件, 回溯upstream
    return;
});

express处理多个中间件:

const app = require("express")();
app.use((req,res,next)=>{
    console.log("first");
    //next();
});
app.use((req,res,next)=>{
    console.log("second");
    //next();
});
app.use((req,res,next)=>{
    console.log("third");
    res.status(200).send("<h1>headers ...</h1>");
});
app.listen(3001);

koa处理多个中间件:

const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
   ctx.body = 'Hello Koa-1';
   next();
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-2';
   next();
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-3';
   next();
 });
app.listen(3000);

/*与express类似,koa中间件的入参也有两个,
后一个就是next。next的功能与express一样*/

/*上面介绍了koa的next()的功能,这里的next()需要同步调用,千万不要采用异步调用
*/

koa捕获异常

异常捕获

const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
app.use((ctx)=>{
  str="hello koa2";//沒有声明变量
  ctx.body=str;
})
app.on("error",(err,ctx)=>{//捕获异常记录错误日志
   console.log(new Date(),":",err);
});
http.createServer(app.callback()).listen(3000);

上面的代码运行后在浏览器访问返回的结果是“Internal Server error”;我们发现当错误发生的时候后端程序并没有死掉,只是抛出了异常,前端也同时接收到了错误反馈,对于KOA来说,异常发生在中间件的执行过程中,所以只要我们在中间件执行过程中将异常捕获并处理就OK了。

添加中间键use方法

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

/*fn可以是三种类型的函数,普通函数,generator函数,
还有async函数。最后generator会被转成async函数,。
所以最终中间件数组只会有普通函数和async函数。*/

异常处理

当异常捕获是有两种处理方式,一种就是响应错误请求,而就是触发注册注册全局错误事件,比如记录错误日志

async 函数

一句话,async 函数就是 Generator 函数的语法糖。

前文有一个 Generator 函数,依次读取两个文件:

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

写成 async 函数,就是下面这样:

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

async 函数的优点
(1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

var result = asyncReadFile();

(2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

async 函数的实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args){
  // ...
}

// 等同于

function fn(args){ 
  return spawn(function*() {
    // ...
  }); 
}

所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。

async 函数的用法
同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  var symbol = await getStockSymbol(name);
  var stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result){
  console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

指定多少毫秒后输出一个值:

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value)
}

asyncPrint('hello world', 50);

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 可能得到错误结果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}
//上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,
//也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

如果确实希望多个请求并发执行,可以使用 Promise.all 方法。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

promise: https://segmentfault.com/n/13...

JS的继承

面试题目:9.js的继承

参考:http://www.ruanyifeng.com/blo...

构造函数的继承

例子:

function Animal(){

    this.species = "动物";

  }

function Cat(name,color){

    this.name = name;

    this.color = color;

  }

一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Cat(name,color){

    Animal.apply(this, arguments);

    this.name = name;

    this.color = color;

  }

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

二、 prototype模式
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了

//将Cat的prototype对象指向一个Animal的实例
//它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。
    Cat.prototype = new Animal();
    
//任何一个prototype对象都有一个constructor属性,指向它的构造函数。
//如果没有"Cat.prototype = new Animal();
//"这一行,Cat.prototype.constructor是指向Cat的;
//加了这一行以后,Cat.prototype.constructor指向Animal。
  Cat.prototype.constructor = Cat;

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

    alert(Cat.prototype.constructor == Animal); //true
    
    //每一个实例也有一个constructor属性,
    //默认调用prototype对象的constructor属性。
     alert(cat1.constructor == Cat.prototype.constructor); // true
     
     //在运行"Cat.prototype = new Animal();"这一行之后, 
      //cat1.constructor也指向Animal!
      alert(cat1.constructor == Animal); // true
      
      //这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须    
     //手动纠正,将Cat.prototype对象的constructor值改为Cat。
     //这就是第二行的意思。

这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

o.prototype = {};
o.prototype.constructor = o;

三、 直接继承prototype

由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

先将Animal对象改写:

function Animal(){ }

Animal.prototype.species = "动物";

然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

Cat.prototype = Animal.prototype;

  Cat.prototype.constructor = Cat;

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

Cat.prototype.constructor = Cat;

// 这一句实际上把Animal.prototype对象的constructor属性也改掉了!

alert(Animal.prototype.constructor); // Cat

四、 利用空对象作为中介

var F = function(){};

  F.prototype = Animal.prototype;

  Cat.prototype = new F();

  Cat.prototype.constructor = Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。

alert(Animal.prototype.constructor); // Animal

将上面的方法,封装成一个函数,便于使用。

function extend(Child, Parent) {

    var F = function(){};

    F.prototype = Parent.prototype;

    Child.prototype = new F();

    Child.prototype.constructor = Child;

    Child.uber = Parent.prototype;

  }

//意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。
//(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,
//可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

使用的时候,方法如下

extend(Cat,Animal);

var cat1 = new Cat("大毛","黄色");

alert(cat1.species); // 动物

五、 拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,把父对象的所有属性和方法,拷贝进子对象

  function Animal(){}

  Animal.prototype.species = "动物";

实现属性拷贝的目的:

function extend2(Child, Parent) {

    var p = Parent.prototype;

    var c = Child.prototype;

    for (var i in p) {

      c[i] = p[i];

      }

    c.uber = p;
    //这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child    
      //对象的prototype对象。

  }

使用的时候,这样写:

extend2(Cat, Animal);

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

非构造函数的继承

例子:

var Chinese = {
    nation:'中国'
  };
  var Doctor ={
    career:'医生'
  }

这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。

object()方法

 function object(o) {

    function F() {}

    F.prototype = o;

    return new F();

  }
//这个object()函数,其实只做一件事,就是把子对象的prototype属性,
//指向父对象,从而使得子对象与父对象连在一起。

使用的时候,第一步先在父对象的基础上,生成子对象:

var Doctor = object(Chinese);

然后,再加上子对象本身的属性:

Doctor.career = '医生';

这时,子对象已经继承了父对象的属性了

 alert(Doctor.nation); //中国

浅拷贝

除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

function extendCopy(p) {

    var c = {};

    for (var i in p) { 
      c[i] = p[i];
    }

    c.uber = p;

    return c;
  }

使用的时候,这样写:

var Doctor = extendCopy(Chinese);

Doctor.career = '医生';

alert(Doctor.nation); // 中国

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

//现在给Chinese添加一个"出生地"属性,它的值是一个数组。
 Chinese.birthPlaces = ['北京','上海','香港'];

//然后,我们为Doctor的"出生地"添加一个城市:
 Doctor.birthPlaces.push('厦门');

//Chinese的"出生地"也被改掉了
alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
alert(Chinese.birthPlaces); //北京, 上海, 香港, 厦门

extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。

深拷贝

所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。

function deepCopy(p, c) {

    var c = c || {};

    for (var i in p) {

      if (typeof p[i] === 'object') {

        c[i] = (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i], c[i]);

      } else {

         c[i] = p[i];

      }
    }

    return c;
  }

使用的时候这样写:

var Doctor = deepCopy(Chinese,Doctor);

现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性

  Chinese.birthPlaces = ['北京','上海','香港'];

  Doctor.birthPlaces.push('厦门');

   alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门

  alert(Chinese.birthPlaces); //北京, 上海, 香港

call和apply的区别

面试题:10.call和apply的区别
参考: http://www.ruanyifeng.com/blo...
https://www.jianshu.com/p/bc5...

this 用法

this是 JavaScript 语言的一个关键字。
它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。

情况一:纯粹的函数调用

这是函数的最通常用法,属于全局性调用,因此this就代表全局对象。

var x = 1;
function test() {
   console.log(this.x);
}
test();  // 1

情况二:作为对象方法的调用

函数还可以作为某个对象的方法调用,这时this就指这个上级对象。

function test() {
  console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;

obj.m(); // 1

情况三 作为构造函数调用

所谓构造函数,就是通过这个函数,可以生成一个新对象。这时,this就指这个新对象。

function test() {
 this.x = 1;
}

var obj = new test();
obj.x // 1

//为了表明这时this不是全局对象,我们对代码做一些改变
var x = 2;
function test() {
  this.x = 1;
}

var obj = new test();
x  // 2
//运行结果为2,表明全局变量x的值根本没变。

情况四 apply 调用

apply()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数。

var x = 0;
function test() {
 console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply() // 0

//如果把最后一行代码修改为
obj.m.apply(obj); //1

call

call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。

var arr = [1, 2, 3, 89, 46]
var max = Math.max.call(null, arr[0], arr[1], arr[2], arr[3], arr[4])//89

例子:

var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

getName.call(obj, 'Dot', 'Dolby')

apply

apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。

var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89

当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量。

例子:

var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

getName.apply(obj, ['Dot', 'Dolby'])// My name is: Dot Dolby

call和apply可用来借用别的对象的方法,这里以call()为例:

var Person1  = function () {
    this.name = 'Dot';
}
var Person2 = function () {
    this.getname = function () {
        console.log(this.name);
    }
    Person1.call(this);
}
var person = new Person2();
person.getname();       // Dot

bind

和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。

var obj = {
    name: 'Dot'
}

function printName() {
    console.log(this.name)
}

var dot = printName.bind(obj)
console.log(dot) // function () { … }
dot()  // Dot

//bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。
 //而原函数printName 中的 this 并没有被改变,依旧指向全局对象 window。

参数的使用

function fn(a, b, c) {
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');

fn('A', 'B', 'C');            // A B C
fn1('A', 'B', 'C');           // Dot A B
fn1('B', 'C');                // Dot B C
fn.call(null, 'Dot');      // Dot undefined undefined

//call 是把第二个及以后的参数作为 fn 方法的实参传进去,
//而 fn1 方法的实参实则是在 bind 中参数的基础上再往后排。

应用场景

求数组中的最大和最小值

var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
var min = Math.min.apply(null,arr)//1

将类数组转化为数组

var trueArr = Array.prototype.slice.call(arrayLike)

数组追加

var arr1 = [1,2,3];
var arr2 = [4,5,6];
var total = [].push.apply(arr1, arr2);//6
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]

判断变量类型

function isArray(obj){
    return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('dot') // false

利用call和apply做继承

function Person(name,age){
    // 这里的this都指向实例
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this,arguments)//将父元素所有方法在这里执行一遍就继承了
}
var dot = new Female('Dot',2)

使用 log 代理 console.log

function log(){
  console.log.apply(console, arguments);
}
// 当然也有更方便的 var log = console.log()

call、apply和bind函数存在的区别

bind返回对应函数, 便于稍后调用; apply, call则是立即调用。

除此外, 在 ES6 的箭头函数下, call 和 apply 将失效, 对于箭头函数来说:

箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于var _this = this这种丑陋的写法
箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误
箭头函数不可以使用 arguments 对象,,该对象在函数体内不存在. 如果要用, 可以用 Rest 参数代替
不可以使用 yield 命令, 因此箭头函数不能用作 Generator 函数

ajax

面试题: 11.ajax是同步还是异步,怎么样实现同步;12.ajax实现过程
参考: https://blog.csdn.net/qq_2956...
https://blog.csdn.net/xxf1597...

Ajax全称Asynchronous JavaScript and XML,也就是异步的js和XML技术。

Ajax的使用四大步骤详解

第一步,创建xmlhttprequest对象

var xmlhttp =new XMLHttpRequest();
//XMLHttpRequest对象用来和服务器交换数据。
var xhttp;

if(window.XMLHttpRequest) {
//现代主流浏览器
xhttp= new XMLHttpRequest();
}else{
//针对浏览器,比如IE5或IE6
xhttp= new ActiveXObject("Microsoft.XMLHTTP");
}

第二步,使用xmlhttprequest对象的open()和send()方法发送资源请求给服务器。
xmlhttp.open(method,url,async) method包括get 和post,url主要是文件或资源的路径,async参数为true(代表异步)或者false(代表同步)

xhttp.send();使用get方法发送请求到服务器。

xhttp.send(string);使用post方法发送请求到服务器。

post 发送请求什么时候能够使用呢?
(1)更新一个文件或者数据库的时候。
(2)发送大量数据到服务器,因为post请求没有字符限制。
(3)发送用户输入的加密数据。

get例子:

xhttp.open("GET","ajax_info.txt",true);

xhttp.open("GET","index.html",true);

xhttp.open("GET","demo_get.asp?t="+ Math.random(), true);xhttp.send();

post例子

xhttp.open("POST", "demo_post.asp", true);

xhttp.send();

post表单例子
post表单数据需要使用xmlhttprequest对象的setRequestHeader方法增加一个HTTP头。

xhttp.open("POST","ajax_test.aspx",true);

xhttp.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");

xhttp.send("fname=Henry&lname=Ford");

async=true 当服务器准备响应时将执行onreadystatechange函数。

xhttp.onreadystatechange= function(){
if(xhttp.readyState == 4 && xhttp.status == 200) {
   document.getElementById("demo").innerHTML=xhttp.responseText;
}

};

xhttp.open("GET", "index.aspx",true);

xhttp.send();

asyn=false 则将不需要写onreadystatechange函数,直接在send后面写上执行代码。

xhttp.open("GET", "index.aspx", false);

xhttp.send();

document.getElementById("demo").innerHTML = xhttp.responseText;

第三步,使用xmlhttprequest对象的responseText或responseXML属性获得服务器的响应。
使用responseText属性得到服务器响应的字符串数据,使用responseXML属性得到服务器响应的XML数据。

例子如下:

document.getElementById("demo").innerHTML = xhttp.responseText;

//服务器响应的XML数据需要使用XML对象进行转换。
xmlDoc= xhttp.responseXML;

txt= "";

x= xmlDoc.getElementsByTagName("ARTIST");

for(i = 0; i < x.length; i++) {
txt+= x[i].childNodes[0].nodeValue + "<br>";
}
document.getElementById("demo").innerHTML= txt;

第四步,onreadystatechange函数
当发送请求到服务器,我们想要服务器响应执行一些功能就需要使用onreadystatechange函数,每次xmlhttprequest对象的readyState发生改变都会触发onreadystatechange函数。
onreadystatechange属性存储一个当readyState发生改变时自动被调用的函数。

readyState属性,XMLHttpRequest对象的状态,改变从0到4,0代表请求未被初始化,1代表服务器连接成功,2请求被服务器接收,3处理请求,4请求完成并且响应准备。
status属性,200表示成功响应,404表示页面不存在。

在onreadystatechange事件中,服务器响应准备的时候发生,当readyState==4和status==200的时候服务器响应准备。

步骤总结

创建XMLHttpRequest对象,也就是创建一个异步调用对象.
创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息.       
设置响应HTTP请求状态变化的函数.       
发送HTTP请求.       
获取异步调用返回的数据.       
使用JavaScript和DOM实现局部刷新.

同步&异步

AJAX中根据async的值不同分为同步(async = false)和异步(async = true)两种执行方式

$.ajax({ 

        type: "post", 

       url: "path", 

       cache:false, 

       async:false, 

        dataType: ($.browser.msie) ? "text" : "xml", 

         success: function(xmlobj){ 

                      function1(){};

        } 

});

 function2(){};

一.什么是同步请求:(false)
同步请求即是当前发出请求后,浏览器什么都不能做,必须得等到请求完成返回数据之后,才会执行后续的代码,相当于是排队,前一个人办理完自己的事务,下一个人才能接着办。也就是说,当JS代码加载到当前AJAX的时候会把页面里所有的代码停止加载,页面处于一个假死状态,当这个AJAX执行完毕后才会继续运行其他代码页面解除假死状态(即当ajax返回数据后,才执行后面的function2)。 
二.什么是异步请求:(true)
  异步请求就当发出请求的同时,浏览器可以继续做任何事,Ajax发送请求并不会影响页面的加载与用户的操作,相当于是在两条线上,各走各的,互不影响。一般默认值为true,异步。异步请求可以完全不影响用户的体验效果,无论请求的时间长或者短,用户都在专心的操作页面的其他内容,并不会有等待的感觉。


同步适用于一些什么情况呢?
我们可以想一下,同步是一步一步来操作,等待请求返回的数据,再执行下一步,那么一定会有一些情况,只有这一步执行完,拿到数据,通过获取到这一步的数据来执行下一步的操作。这是异步没有办法实现的,因此同步的存在一定有他存在的道理。
我们在发送AJAX请求后,还需要继续处理服务器的响应结果,如果这时我们使用异步请求模式同时未将结果的处理交由另一个JS函数进行处理。这时就有可能发生这种情况:异步请求的响应还没有到达,函数已经执行完了return语句了,这时将导致return的结果为空字符串。

闭包

面试题:13.闭包的作用理解,以及那些地方用过闭包,以及闭包的缺点,如何实现闭包
参考:https://segmentfault.com/a/11...

闭包的作用理解

变量的作用域

变量的作用域无非就是两种:全局变量和局部变量。Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

 var n=999;
  function f1(){
    alert(n);
  }
  f1(); // 999

//另一方面,在函数外部自然无法读取函数内的局部变量。
  function f1(){
    var n=999;
  }

  alert(n); // error
//这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

从外部读取局部变量

 function f1(){

    var n=999;

    function f2(){
      alert(n); // 999
    }

  }
/*在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,
对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。*/
/*这就是Javascript语言特有的"链式作用域"结构(chain scope),
子对象会一级一级地向上寻找所有父对象的变量。
所以,父对象的所有变量,对子对象都是可见的,反之则不成立。*/

闭包的概念

闭包就是能够读取其他函数内部变量的函数。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。


闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。


使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包的使用场景

应用场景一:setTimeout
原生的setTimeout有一个缺陷,你传递的第一个函数不能带参数。即

setTimeout(func(parma),1000);

我们就可以用闭包来实现这个效果了

function func(param) {
    return function() {
        alert(param);
    }
}
var f = func(1)
setTimeout(f, 1000);

应用场景二:用闭包模拟私有方法

// 可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行毕,就可以立即销毁其作用域链了
(function(){
    function createFunc() {
        var name = "wheeler";
        return function () {
            return name;
        }
    }

    var nameFunc = createFunc();

    var name = nameFunc();

    console.log(name);
})();
闭包的应用场景
用闭包模拟私有方法
var returnNum = (function () {
    var num = 0;

    function changeNum(value) {
        num = value;
    }

    return {
        add: function () {
            changeNum(10);
        },
        delete: function () {
            changeNum(-10);
        },
        getNum: function () {
            return num;
        }
    }
})();

// 闭包
console.log(returnNum.getNum());
returnNum.add();
console.log(returnNum.getNum());
returnNum.delete();
console.log(returnNum.getNum());

应用场景三:缓存

var CacheCount = (function () {
    var cache = {};
    return {
        getCache: function (key) {
            if (key in cache) {// 如果结果在缓存中
                return cache[key];// 直接返回缓存中的对象
            }
            var newValue = getNewValue(key); // 外部方法,获取缓存
            cache[key] = newValue;// 更新缓存
            return newValue;
        }
    };
})();

console.log(CacheCount.getCache("key1"));

应用场景四:封装

var person = function(){
    var name = "default";//变量作用域为函数内部,外部无法访问
    return {
        getName : function(){
            return name;
        },
        setName : function(newName){
            name = newName;
        }
    }
}();

console.log(person.name);// undefined
console.log(person.getName());
person.setName("wheeler");
console.log(person.getName());

跨域

面试题:14.跨域方法以及怎么样实现的与原理
参考:http://www.ruanyifeng.com/blo...
https://blog.csdn.net/qq_3409...
https://blog.csdn.net/qq_2860...
http://www.ruanyifeng.com/blo...

同源政策

所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

举例

http://www.example.com/dir/page.html

这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略)。它的同源情况如下:

http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)

限制范围
目前,如果非同源,共有三种行为受到限制:

  • Cookie、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。

但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。

举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。

document.domain = 'example.com';
//现在,A网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello";

//B网页就可以读到这个 Cookie。
var allCookie = document.cookie;
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/

iframe
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

//比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame

//子窗口获取主窗口的DOM也会报错。
window.parent.document.body
// 报错

如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到DOM。

例子:

/* 1. 在页面 http://a.example.com/a.html 设置document.domain */

<iframe id = "iframe" data-original="http://b.example.com/b.html" onload = "test()"></iframe>
<script type="text/javascript">
    document.domain = 'example.com';//设置成主域
    function test(){
        alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
    }
</script>

/* 2. 在页面http:// b.example.com/b.html 中设置document.domain */

<script type="text/javascript">
    document.domain = 'example.com';//在iframe载入这个页面也设置document.domain,使之与主页面的document.domain相同
</script>

对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题:

  • 片段识别符(fragment identifier)
  • window.name
  • 跨文档通信API(Cross-document messaging)

片段识别符

片段标识符(fragment identifier)指的是,URL的#号后面的部分,比如http://example.com/x.html#fra...。如果只是改变片段标识符,页面不会重新刷新。
父窗口可以把信息,写入子窗口的片段标识符。

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

//子窗口通过监听hashchange事件得到通知。
window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}
//同样的,子窗口也可以改变父窗口的片段标识符。
parent.location.href= target + "#" + hash;

window.name

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

//父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name属性。
window.name = data;

//接着,子窗口跳回一个与主窗口同域的网址。
location = 'http://parent.url.com/xxx.html';

//主窗口就可以读取子窗口的window.name了。
var data = document.getElementById('myFrame').contentWindow.name;
这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

例子:
www.test.com下a.html页:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>

        <button>click!</button>

        <script type="text/javascript">

            var a=document.getElementsByTagName("button")[0];

            a.onclick=function(){                               //button添加click事件
                var inf=document.createElement("iframe");       //创建iframe
                inf.data-original="http://www.domain.com/window.name/b.html"+"?h=5"  //加载数据页www.domain.com/window.name/b.html同事携带参数h=5
                var body=document.getElementsByTagName("body")[0];
                body.appendChild(inf);                          //引入a页面

                inf.onload=function(){
                    inf.data-original='http://www.test.com/b.html'       //iframe加载完成,加载www.test.com域下边的空白页b.html
                    console.log(inf.contentWindow.name)        //输出window.name中的数据
                    body.removeChild(inf)                      //清除iframe
                }
            }

        </script>
    </body>
</html>

www.domain.com下b.html页:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <a href="" target="" title="">2</a>
        <script type="text/javascript" data-original="../cross/jquery-2.2.3.js">

        </script>
        <script>
            var str=window.location.href.substr(-1,1);      //获取url中携带的参数值
            $.ajax({
                type:"get",
                url:"http://www.domain.com/a.php"+"?m="+str, //通过ajax将查询参数传给php页面
                async:true,
                success:function(res){
                    window.name=res                         //将接收到的查询数据赋值给window.name
                },
                error:function(){
                    window.name='error'                      //..
                }
            });
        </script>
    </body>
</html>

window.postMessage

这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。

window.opener.postMessage('Nice to see you', 'http://aaa.com');

//父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
  console.log(e.data);
},false);

message事件的事件对象event,提供以下三个属性:

  1. event.source:发送消息的窗口
  2. event.origin: 消息发向的网址
  3. event.data: 消息内容

子窗口通过event.source属性引用父窗口,然后发送消息:

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}

event.origin属性可以过滤不是发给本窗口的消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}

postMessage的使用方法: otherWindow.postMessage(message, targetOrigin);

otherWindow: 指目标窗口,也就是给哪个window发消息,是
window.frames 属性的成员或者由 window.open 方法创建的窗口
message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
targetOrigin: 是限定消息接收范围,不限制请使用 ‘*’


例子:

//本地代码index.html

<body>  
    <iframe id="proxy" data-original="http://server.com/remote.html" onload = "postMsg()" style="display: none" ></iframe>  
    <script type="text/javascript">  
        var obj = {  
            msg: 'hello world'  
        }  
        function postMsg (){  
            var iframe = document.getElementById('proxy');  
            var win = iframe.contentWindow;  
            win.postMessage(obj,'http://server.com');  
        }  
    </script>  
</body>
//server.com上remote.html,监听message事件,并检查来源是否是要通信的域。

<head>
    <title></title>
    <script type="text/javascript">
        window.onmessage = function(e){
            if(e.origin !== 'http://localhost:8088') return;
            alert(e.data.msg+" from "+e.origin);
        }
    </script>
</head>

LocalStorage

主窗口写入iframe子窗口的localStorage

//子窗口将父窗口发来的消息,写入自己的LocalStorage。
window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

//父窗口发送消息
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');

加强版的子窗口接收消息:

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加强版的父窗口发送消息代码:

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 读取对象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  // "Jack"
  console.log(JSON.parse(e.data).name);
};

location.hash

这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。www.a.com下的a.html想和www.b.com下的b.html通信(在a.html中动态创建一个b.html的iframe来发送请求)
但是由于“同源策略”的限制他们无法进行交流(b.html无法返回数据),于是就找个中间人:www.a.com下的c.html(注意是www.a.com下的)。
b.html将数据传给c.html(b.html中创建c.html的iframe),由于c.html和a.html同源,于是可通过c.html将返回的数据传回给a.html,从而达到跨域的效果。

clipboard.png

//a.html

<script>
function startRequest(){  
    var ifr = document.createElement('iframe');  
    ifr.style.display = 'none';  
    ifr.src = 'http://www.b.com/b.html#sayHi'; //传递的location.hash 
    document.body.appendChild(ifr);  
}  
function checkHash() {  
    try {  
        var data = location.hash ? location.hash.substring(1) : '';  
        if (console.log) {  
            console.log('Now the data is '+data);  
        }  
    } catch(e) {};  
}  
setInterval(checkHash, 2000); 
window.onload = startRequest;
</script>
//b.html

<script>
function checkHash(){
  var data = '';
  //模拟一个简单的参数处理操作
  switch(location.hash){
    case '#sayHello': data = 'HelloWorld';break;
    case '#sayHi': data = 'HiWorld';break;
    default: break;
  }
  data && callBack('#'+data);
}
function callBack(hash){
  // ie、chrome的安全机制无法修改parent.location.hash,所以要利用一个中间的www.a.com域下的代理iframe
  var proxy = document.createElement('iframe');
  proxy.style.display = 'none';
  proxy.src = 'http://localhost:8088/proxy.html'+hash;  // 注意该文件在"www.a.com"域下
  document.body.appendChild(proxy);
}
window.onload = checkHash;
</script>
//由于两个页面不在同一个域下,IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe,这里有一个a.com下的代理文件c.html。Firefox可以修改。
//c.html
<script>parent.parent.location.hash = self.location.hash.substring(1);  </script>

直接访问a.html,a.html向b.html发送的消息为”sayHi”;b.html通过消息判断返回了”HiWorld”,并通过c.html改变了location.hash的值

clipboard.png

AJAX
同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。

  1. JSONP
  2. WebSocket
  3. CORS

JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

首先,网页动态插入<script>元素,由它向跨源网址发出请求。

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};
//上面代码通过动态添加<script>元素,向服务器example.com发出请求。
//注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,
//这对于JSONP是必需的。

//服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({
  "ip": "8.8.8.8"
});

WebSocket
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
具体实例:https://segmentfault.com/a/11...

跨域资源共享 CORS

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求:(只要同时满足以下两大条件)
(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、
    multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

/*上面的头信息中,Origin字段用来说明,
本次请求来自哪个源(协议 + 域名 + 端口)。
服务器根据这个值,决定是否同意这次请求。*/

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

withCredentials 属性

,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

快排

"快速排序"的思想很简单,整个排序过程只需要三步:
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

例子

举例来说,现在有一个数据集{85, 24, 63, 45, 17, 31, 96, 50},怎么对其排序呢?

第一步,选择中间的元素45作为"基准"。(基准值可以任意选择,但是选择中间的值比较容易理解。)

clipboard.png
第二步,按照顺序,将每个元素与"基准"进行比较,形成两个子集,一个"小于45",另一个"大于等于45"。

clipboard.png
第三步,对两个子集不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

clipboard.png

clipboard.png

clipboard.png

//首先,定义一个quickSort函数,它的参数是一个数组。

var quickSort = function(arr) {

};
//然后,检查数组的元素个数,如果小于等于1,就返回。
var quickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

};
/*接着,选择"基准"(pivot),并将其与原数组分离,
再定义两个空数组,用来存放一左一右的两个子集。*/
var quickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

  var pivotIndex = Math.floor(arr.length / 2) ;

  var pivot = arr.splice(pivotIndex, 1)[0];

  var left = [];

  var right = [];

};
//开始遍历数组,小于"基准"的元素放入左边的子集,大于基准的元素放入右边的子集。
for (var i = 0; i < arr.length; i++){

    if (arr[i] < pivot) {

      left.push(arr[i]);

    } else {

      right.push(arr[i]);

    }

  }
//使用递归不断重复这个过程,就可以得到排序后的数组

js的事件机制

参考:https://www.cnblogs.com/lazyc...
clipboard.png


DOM0级事件处理程序

 var btn5 = document.getElementById('btn5');
 btn5.onclick=function(){
    console.log(this.id);//btn5   
 };

基于DOM0的事件,对于同一个dom节点而言,只能注册一个,后边注册的 同种事件 会覆盖之前注册的。利用这个原理我们可以解除事件,btn5.onclick=null;其中this就是绑定事件的那个元素;

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理;

DOM2级事件处理程序

DOM2支持同一dom元素注册多个同种事件,事件发生的顺序按照添加的顺序依次触发(IE是相反的)。
DOM2事件通过addEventListener和removeEventListener管理

addEventListener(eventName,handlers,boolean);removeEventListener(),两个方法都一样接收三个参数,第一个是要处理的事件名,第二个是事件处理程序,第三个值为false时表示在事件冒泡阶段调用事件处理程序,一般建议在冒泡阶段使用,特殊情况才在捕获阶段;
注意:通过addEventListener()添加的事件处理程序只能用removeEventListener()来移除,并且移除时传入的参数必须与添加时传入的参数一样;

var btn2 = document.getElementById('btn2');
var handlers = function () {
   console.log(this.id);
};

btn2.addEventListener('click',handlers,false);

btn2.removeEventListener('click',handlers,false);

IE事件处理程序

IE用了attachEvent(),detachEvent(),接收两个参数,事件名称和事件处理程序,通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段,所以平时为了兼容更多的浏览器最好将事件添加到事件冒泡阶段,IE8及以前只支持事件冒泡;

 var btn3 = document.getElementById('btn3');
 var handlers2=function(){
    console.log(this===window);//true,注意attachEvent()添加的事件处理程序运行在全局作用域中;
 };
 btn3.attachEvent('onclick',handlers2);

跨浏览器事件处理程序

//创建的方法是addHandlers(),removeHandlers(),这两个方法属于一个叫EventUtil的对象;但是这个没有考虑到IE中作用域的问题,不过就添加和移除事件还是足够的。
 
var EventUtil = {
   addHandlers: function (element, type, handlers) {
      if (element.addEventListener) {
         element.addEventListener(type, handlers, false);
      } else if (element.attachEvent) {
         element.attachEvent(on + type, handlers);
      } else {
         element['on' + type] = handlers;
      }
   },
   removeHandlers: function (element, type, handlers) {
      if (element.removeEventListener) {
         element.removeEventListener(type, handlers, false);
      } else if (element.detachEvent) {
         element.detachEvent(on + type, handlers);
      } else {
         element['on' + type] = null;
      }
   }
};

例子:

var btn4=document.getElementById('btn4');
var handlers3=function(){
   console.log('123')
};
EventUtil.addHandlers(btn4,'click',handlers3);
//……
EventUtil.removeHandlers(btn4,'click',handlers3);

事件对象

兼容触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含了所有与事件有关的信息,比如导致事件的元素target,事件的类型,及其他特定的相关信息。例如鼠标操作导致的事件对象中会包含鼠标的位置,单双击等,而键盘操作导致的事件对象会包含按下的键等信息;

事件被触发时,会默认给事件处理程序传入一个参数e , 表示事件对象;通过e,我们可以获得其中包含的与事件有关的信息;

只有在事件处理程序执行期间,event对象才会存在,一旦事件处理程序执行完毕,event对象就会被销毁;


DOM中的事件对象

兼容DOM的浏览器会自动将一个事件对象event传递给事件处理程序

(注:event.target不支持IE浏览器,应该用event.srcElement;还有 IE中通过attachment添加的事件是运行在全局作用域中的,this===window)

当事件绑定在真正的目标元素上时,this===target===currentTarget ,而且绑定事件时是否捕获结果都是一样的,此时eventParse==2;

event.stopPropagation()可以阻止事件的传播,但它阻止不了绑定在该元素上的其他函数的执行
event.stopImmediatePropagation()阻止所有的事件执行

IE中的事件对象

IE中event参数是未定的,事件对象是作为window的一个属性存在的,因此可以通过window.event来访问event对象,不同于DOM级中event是作为参数直接传入和返回;

![图片上传中...]

事件委托

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JS代码之间就会建立一个连接,而这种连接越多,页面执行起来就越慢。考虑内存和性能问题,为了解决事件处理程序过多的问题,采用事件委托变得很有必要。
冒泡机制,比如既然点击子元素,也会触发父元素的点击事件,那我们完全可以将子元素的事件要做的事写到父元素的事件里,也就是将子元素的事件处理程序写到父元素的事件处理程序中,这就是事件委托;利用事件委托,只指定一个事件处理程序,就可以管理某一个类型的所有事件;

例子:

window.onload = function(){
  var oUl = document.getElementById("ul");
  var aLi = oUl.getElementsByTagName("li");

/*
这里要用到事件源:event 对象,事件源,不管在哪个事件中,只要你操作的那个元素就是事件源。
ie:window.event.srcElement
标准下:event.target
nodeName:找到元素的标签名
*/
  oUl.onmouseover = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    //alert(target.innerHTML);
    if(target.nodeName.toLowerCase() == "li"){
    target.style.background = "red";
    }
  }
  oUl.onmouseout = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    //alert(target.innerHTML);
    if(target.nodeName.toLowerCase() == "li"){
    target.style.background = "";
    }
  }
}

几种最适合采用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress

因为把事件绑定到了父节点上,因此省了绑定事件。就算后面新增的子节点也有了相关事件,删除部分子节点不用去销毁对应节点上绑定的事件
父节点是通过event.target来找对应的子节点的。(事件处理程序中的this值始终等于currentTarget的值,指向的是绑定到的那个元素)

JQ中的事件委托

html:
<ul id = "lists">
         <li>列表1</li>
         <li>列表2</li>
         <li>列表3</li>
         <li>列表4</li>
         <li>列表5</li>
         <li>列表6</li>
 </ul>
JS:
var lists = document.getElementById("lists");

        lists.addEventListener("click",function(event){

            var target = event.target;
            //防止父元素ul也触发事件
            if(target.nodeName == "LI"){
               target.style.backgroundColor = "red";
            }
        })
JQ用on方法:

$(function(){
            $("#lists").on("click","li",function(event){
                var target = $(event.target);
                target.css("background-color","red");
            })
        })
用delegate()方法:

$(function(){
            $("#lists").delegate("li","click",function(event){
                var target = $(event.target);
                target.css("background-color","red");
            })
        })

on()方法和delegate()方法对于事件委托的写法很像。并且执行事件委托的时候只有子元素(本文中的li)会触发事件,而代为执行的父元素(本文中为ul)不会触发事件,所以我们不需要盘判断触发事件的元素节点名,这一点明显优于原生的JavaScript。

用bind()方法:

$(function(){
            $("#lists").bind("click","li",function(event){
                var target = $(event.target);
                if(target.prop("nodeName")=="LI"){
                target.css("background-color","red");}
            })
        })

bind()方法同原生的JavaScript实现方法一样,当父元素代子元素执行事件时,父元素也会 触发事件,所以我们需要判断一下触发事件的元素名。此外,用bind()方法给元素绑定事件的时候要注意,它只能给已经存在DOM元素添加事件,不能给未来存在DOM

元素添加添加事件。如果要频繁地添加DOM元素,并且给新添加的DOM元素绑定事件的话,用on()方法

数组去重

方法一:

双层循环,外层循环元素,内层循环时比较值
如果有相同的值则跳过,不相同则push进数组

Array.prototype.distinct = function(){
 var arr = this,
  result = [],
  i,
  j,
  len = arr.length;
 for(i = 0; i < len; i++){
  for(j = i + 1; j < len; j++){
   if(arr[i] === arr[j]){
    j = ++i;
   }
  }
  result.push(arr[i]);
 }
 return result;
}
var arra = [1,2,3,4,4,1,1,2,1,1,1];
arra.distinct();    //返回[3,4,2,1]

方法二:利用splice直接在原数组进行操作

双层循环,外层循环元素,内层循环时比较值
值相同时,则删去这个值
注意点:删除元素之后,需要将数组的长度也减1.

Array.prototype.distinct = function (){
 var arr = this,
  i,
  j,
  len = arr.length;
 for(i = 0; i < len; i++){
  for(j = i + 1; j < len; j++){
   if(arr[i] == arr[j]){
    arr.splice(j,1);
    len--;
    j--;
   }
  }
 }
 return arr;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,];
var b = a.distinct();
console.log(b.toString()); //1,2,3,4,5,6,56

优点:简单易懂
缺点:占用内存高,速度慢

方法三:利用对象的属性不能相同的特点进行去重

Array.prototype.distinct = function (){
 var arr = this,
  i,
  obj = {},
  result = [],
  len = arr.length;
 for(i = 0; i< arr.length; i++){
  if(!obj[arr[i]]){ //如果能查找到,证明数组元素重复了
   obj[arr[i]] = 1;
   result.push(arr[i]);
  }
 }
 return result;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,];
var b = a.distinct();
console.log(b.toString()); //1,2,3,4,5,6,56

方法四:数组递归去重

先排序,然后从最后开始比较,遇到相同,则删除

Array.prototype.distinct = function (){
 var arr = this,
  len = arr.length;
 arr.sort(function(a,b){  //对数组进行排序才能方便比较
  return a - b;
 })
 function loop(index){
  if(index >= 1){
   if(arr[index] === arr[index-1]){
    arr.splice(index,1);
   }
   loop(index - 1); //递归loop函数进行去重
  }
 }
 loop(len-1);
 return arr;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,56,45,56];
var b = a.distinct();
console.log(b.toString());  //1,2,3,4,5,6,45,56

方法五:利用indexOf以及forEach

Array.prototype.distinct = function (){
 var arr = this,
  result = [],
  len = arr.length;
 arr.forEach(function(v, i ,arr){  //这里利用map,filter方法也可以实现
  var bool = arr.indexOf(v,i+1);  //从传入参数的下一个索引值开始寻找是否存在重复
  if(bool === -1){
   result.push(v);
  }
 })
 return result;
};
var a = [1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,3,2,3,3,2,2,1,23,1,23,2,3,2,3,2,3];
var b = a.distinct();
console.log(b.toString()); //1,23,2,3

方法六:利用ES6的set

function dedupe(array){
 return Array.from(new Set(array));
}
dedupe([1,1,2,3]) //[1,2,3]

拓展运算符(...)内部使用for...of循环

let arr = [1,2,3,3];
let resultarr = [...new Set(arr)]; 
console.log(resultarr); //[1,2,3]

一、concat()方法

思路:concat() 方法将传入的数组或非数组值与原数组合并,组成一个新的数组并返回。该方法会产生一个新的数组。

function concatArr(arr1, arr2){
  var arr = arr1.concat(arr2);
  arr = unique1(arr);//再引用上面的任意一个去重方法
  return arr;
}

二、Array.prototype.push.apply()

思路:该方法优点是不会产生一个新的数组

var a = [1, 2, 3];
var b = [4, 5, 6];
Array.prototype.push.apply(a, b);//a=[1,2,3,4,5,6]
//等效于:a.push.apply(a, b);
//也等效于[].push.apply(a, b); 
function concatArray(arr1,arr2){
  Array.prototype.push.apply(arr1, arr2);
  arr1 = unique1(arr1);
  return arr1;
}

filter

arr.filter(function(element,index){
return index === arr.indexOf(element)
})

补充es6知识点

参考:https://www.cnblogs.com/chris...
https://www.cnblogs.com/rlann...

解构:

解构的作用是可以快速取得数组或对象当中的元素或属性,而无需使用arr[x]或者obj[key]等传统方式进行赋值

//数组解构赋值:

var arr = ['this is a string', 2, 3];

//传统方式
var a = arr[0],
    b = arr[1],
    c = arr[2];

//解构赋值,是不是简洁很多?
var [a, b, c] = arr;

console.log(a);//this is a string
console.log(b);//2
console.log(c);//3

//嵌套数组解构:
var arr = [[1, 2, [3, 4]], 5, 6];
var [[d, e, [f, g]], h, i] = arr;
console.log(d);//1
console.log(f);//3
console.log(i);//6

//函数传参解构:
var arr = ['this is a string', 2, 3];

function fn1([a, b, c]) {
    console.log(a);
    console.log(b);
    console.log(c);
}

fn1(arr);
//this is a string
//2
//3

//for循环解构:

var arr = [[11, 12], [21, 22], [31, 32]];
for (let [a, b] of arr) {
    console.log(a);
    console.log(b);
}
//11
//12
//21
//22
//31
//32

//对象赋值解构:
var obj = {
    name: 'chris',
    sex: 'male',
    age: 26,
    son: {
        sonname: '大熊',
        sonsex: 'male',
        sonage: 1
    }
};

function fn2({sex, age, name}) {
    console.log(name + ' ' + sex + ' ' + age);
}

fn2(obj);
//chris male 26

//变量名与对象属性名不一致解构:
var obj = {
    name: 'chris',
    sex: 'male',
    age: 26
};
var {name: nickname, age: howold} = obj;
console.log(nickname + ' ' + howold); //chris 26

//嵌套对象解构:
var obj = {
    name: 'chris',
    sex: 'male',
    age: 26,
    son: {
        sonname: '大熊',
        sonsex: 'male',
        sonage: 1
    }
};
var {name, sex, age, son: {sonname, sonsex, sonage}} = obj;
console.log(sonname + ' ' + sonsex + ' ' + sonage);
//大熊 male 1

//Babel暂不支持这种嵌套解构
obj = {
    name: 'chris',
    sex: 'male',
    age: [1, 2, 3]
}

{name, sex, age: [a, b, c]} = obj;
console.log(c);

//嵌套对象属性重名,解构时需要更改变量名:
name: 'chris',
    sex: 'male',
    age: 26,
    son: {
        name: '大熊',
        sex: 'male',
        age: 1
    }
};
//赋值解构
var {name: fathername, son: {name, sex, age}} = obj;
console.log(fathername); //chris
console.log(name); //大熊

//传参解构
function fn3({sex, age, name, son: {name: sonname}}) {
    console.log(name + ' ' + sex + ' ' + age + ' ' + sonname);
}

fn3(obj);
//chris male 26 大熊

//循环解构对象:
var arr = [{name: 'chris', age: 26}, {name: 'jack',    age: 27}, {name: 'peter',age: 28}];

for (let {age, name} of arr) {
    console.log(name + ' ' + age);
}
//chris 26
//jack 27
//peter 28

//解构的特殊应用场景:
//变量互换
var x = 1,
    y = 2;
var [x, y] = [y, x];
console.log(x); //2
console.log(y); //1

//字符串解构
var str = 'love';
var [a, b, c, d] = str;
console.log(a);//l
console.log(b);//o
console.log(c);//v
console.log(d);//e

扩展运算符

扩展运算符用三个点号表示,功能是把数组或类数组对象展开成一系列用逗号隔开的值

var foo = function(a, b, c) {
    console.log(a);
    console.log(b);
    console.log(c);
}

var arr = [1, 2, 3];

//传统写法
foo(arr[0], arr[1], arr[2]);

//使用扩展运算符
foo(...arr);
//1
//2
//3

特殊应用场景:

//数组深拷贝
var arr2 = arr;
var arr3 = [...arr];
console.log(arr===arr2); //true, 说明arr和arr2指向同一个数组
console.log(arr===arr3); //false, 说明arr3和arr指向不同数组

//把一个数组插入另一个数组字面量
var arr4 = [...arr, 4, 5, 6];
console.log(arr4);//[1, 2, 3, 4, 5, 6]

//字符串转数组
var str = 'love';
var arr5 = [...str];
console.log(arr5);//[ 'l', 'o', 'v', 'e' ]

应用:

//将一个数组转为用逗号分隔的参数序列
console.log(...[a, b, c])  
// a b c

//将一个数组,变为参数序列
 let add = (x, y) => x + y;
            let numbers = [3, 45];
            console.log(add(...numbers))//48
            
//使用扩展运算符展开数组代替apply方法,将数组转为函数的参数
//ES5 取数组最大值
console.log(Math.max.apply(this, [654, 233, 727]));
//ES6 扩展运算符
console.log(Math.max(...[654, 233, 727]))
//相当于
console.log(Math.max(654, 233, 727))

//使用push将一个数组添加到另一个数组的尾部
// ES5  写法  
var arr1 = [1, 2, 3];  
var arr2 = [4, 5, 6];  
Array.prototype.push.apply(arr1, arr2); 

//push方法的参数不能是数组,通过apply方法使用push方法 
// ES6  写法  
let arr1 = [1, 2, 3];  
let arr2 = [4, 5, 6];  
arr1.push(...arr2); 

//合并数组
var arr1 = ['a', 'b'];  
var arr2 = ['c'];  
var arr3 = ['d', 'e'];  
// ES5 的合并数组  
arr1.concat(arr2, arr3);  
// [ 'a', 'b', 'c', 'd', 'e' ]  
// ES6 的合并数组  
[...arr1, ...arr2, ...arr3]  
// [ 'a', 'b', 'c', 'd', 'e' ] 

//将字符串转换为数组
[...'hello']  
// [ "h", "e", "l", "l", "o" ] 
//ES5
str.split('')

//转换伪数组为真数组
var nodeList = document.querySelectorAll('p');  
var array = [...nodeList]; 
//具有iterator接口的伪数组,非iterator对象用Array.from方法

//map结构
let map = new Map([  
[1, 'one'],  
[2, 'two'],  
[3, 'three'],  
]);  
let arr = [...map.keys()]; // [1, 2, 3]

rest运算符

rest运算符也是三个点号,不过其功能与扩展运算符恰好相反,把逗号隔开的值序列组合成一个数组

//主要用于不定参数,所以ES6开始可以不再使用arguments对象
var bar = function(...args) {
    for (let el of args) {
        console.log(el);
    }
}

bar(1, 2, 3, 4);
//1
//2
//3
//4

bar = function(a, ...args) {
    console.log(a);
    console.log(args);
}

bar(1, 2, 3, 4);
//1
//[ 2, 3, 4 ]

rest运算符配合解构使用:

var [a, ...rest] = [1, 2, 3, 4];
console.log(a);//1
console.log(rest);//[2, 3, 4]

对于三个点号,三点放在形参或者等号左边为rest运算符; 放在实参或者等号右边为spread运算符,或者说,放在被赋值一方为rest运算符,放在赋值一方为扩展运算符。

  • 在等号赋值或for循环中,如果需要从数组或对象中取值,尽量使用解构。
  • 在自己定义函数的时候,如果调用者传来的是数组或对象,形参尽量使用解构方式,优先使用对象解构,其次是数组解构。代码可读性会很好。
  • 在调用第三方函数的时候,如果该函数接受多个参数,并且你要传入的实参为数组,则使用扩展运算符。可以避免使用下标形式传入参数。也可以避免很多人习惯的使用apply方法传入数组。
  • rest运算符使用场景应该稍少一些,主要是处理不定数量参数,可以避免arguments对象的使用。

箭头函数

参考:https://developer.mozilla.org...

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。

//更短的函数

var materials = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

materials.map(function(material) { 
  return material.length; 
}); // [8, 6, 7, 9]

materials.map((material) => {
  return material.length;
}); // [8, 6, 7, 9]

materials.map(material => material.length); // [8, 6, 7, 9]

不绑定this:

function Person() {
  // Person() 构造函数定义 `this`作为它自己的实例.
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式, growUp()函数定义 `this`作为全局对象, 
    // 与在 Person()构造函数中定义的 `this`并不相同.
    this.age++;
  }, 1000);
}

var p = new Person();

//在ECMAScript 3/5中,通过将this值分配给封闭的变量,可以解决this问题。
function Person() {
  var that = this;
  that.age = 0;

  setInterval(function growUp() {
    //  回调引用的是`that`变量, 其值是预期的对象. 
    that.age++;
  }, 1000);
}

//箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。
function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| 正确地指向person 对象
  }, 1000);
}

var p = new Person();

通过 call 或 apply 调用

由于 箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this---译者注),他们的第一个参数会被忽略。(这种现象对于bind方法同样成立---译者注)

var adder = {
  base : 1,
    
  add : function(a) {
    var f = v => v + this.base;
    return f(a);
  },

  addThruCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base : 2
    };
            
    return f.call(b, a);
  }
};

console.log(adder.add(1));         // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2(而不是3 ——译者注

不绑定arguments

var arguments = [1, 2, 3];
var arr = () => arguments[0];

arr(); // 1

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的 arguments 对象. arguments[0] 是 n
  return f();
}

foo(1); // 2

箭头函数也可以使用条件(三元)运算符:

var simple = a => a > 15 ? 15 : a;
simple(16); // 15
simple(10); // 10

let max = (a, b) => a > b ? a : b;

箭头函数也可以使用闭包:

// 标准的闭包函数
function A(){
      var i=0;
      return function b(){
              return (++i);
      };
};

var v=A();
v();    //1
v();    //2


//箭头函数体的闭包( i=0 是默认参数)
var Add = (i=0) => {return (() => (++i) )};
var v = Add();
v();           //1
v();           //2

//因为仅有一个返回,return 及括号()也可以省略
var Add = (i=0)=> ()=> (++i);

模板字符串

使用美元符号和大括号包裹变量${对象名.属性名}

es5:

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);
es6:

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

模板字符串中嵌入变量,需要将变量名写在${}之中。(注:不声明会报错)

let name = 'jim',
     age = 18,
     gender = 'male';
console.log(`name : ${name} , age : ${age} , gender : ${gender}`)

大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性,而且还能调用函数

let x = 1,
    y = 2;
console.log(`${x} + ${y * 2} = ${x + y * 2}`)//  "1 + 4 = 5"


let obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`)//  3


function fn() {
    return "Hello World";
        }
console.log(`foo ${fn()} bar`)// foo Hello World bar

模板字符串还可以嵌套

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;
查看原文

tryzf 回答了问题 · 2018-09-13

dva + roadhog 修改文件不会自动刷新页面

'react-dev-utils/webpackHotDevClient'解决, entry修改如下:

  entry: {
    app: ['react-dev-utils/webpackHotDevClient', path.resolve(__dirname, 'src/polyfill.js'), path.resolve(__dirname, 'src/index.js')]
  }

关注 1 回答 1

tryzf 提出了问题 · 2018-08-28

dva + roadhog 修改文件不会自动刷新页面

const path = require('path');

export default {
  // entry: 'src/index.js',    // 用这个可以自动刷新
  entry: {
    app: [path.resolve(__dirname, 'src/polyfill.js'), path.resolve(__dirname, 'src/index.js')]   // 用这里不可以自动刷新
  },
  extraBabelPlugins: [
    ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }],
  ],
  env: {
    development: {
      extraBabelPlugins: ['dva-hmr'],
    },
  },
  alias: {
    'components': path.resolve(__dirname, 'src/components/'),
    'utils': path.resolve(__dirname, 'src/utils/'),
    'assets': path.resolve(__dirname, 'src/assets'),
    'services': path.resolve(__dirname, 'src/services'),
    'pages': path.resolve(__dirname, 'src/pages'),
    'layouts': path.resolve(__dirname, 'src/layouts'),
    'models': path.resolve(__dirname, 'src/models'),
    '@': path.resolve(__dirname, 'src'),
  },
  ignoreMomentLocale: true,
  html: {
    template: './src/index.ejs',
  },
  lessLoaderOptions: {
    javascriptEnabled: true,
  },
  disableDynamicImport: true,
  publicPath: '/',
  hash: true,
  extraBabelIncludes: ['react-native-progress'],
};

为了兼容ie, 集成了一个polyfill.js, 但自从.webpackrc.js配置修改如上后,文件修改就不能自动刷新。(在index.js, import polyfill.js 又不起作用)

// polyfill.js
import '@babel/polyfill';
import 'url-polyfill';
import setprototypeof from 'setprototypeof';

// React depends on set/map/requestAnimationFrame
// https://reactjs.org/docs/javascript-environment-requirements.html
import 'core-js/es6/promise';
import 'core-js/es6/set';
import 'core-js/es6/map';
// import 'raf/polyfill'; 
//只兼容到IE10不需要,况且fetch的polyfill whatwg-fetch也只兼容到IE10

// https://github.com/umijs/umi/issues/413
Object.setPrototypeOf = setprototypeof;

这个问题是否有人遇到过,求解!!

关注 1 回答 1

tryzf 关注了用户 · 2018-06-11

Aresn @aresn

正直 进取 合作 创新

关注 1748

tryzf 回答了问题 · 2018-06-07

解决react里面点击改变变量,为啥这么写不起作用?

this.setState({like: !this.state.like})

关注 6 回答 5

tryzf 收藏了文章 · 2018-04-07

每个 JavaScript 工程师都应当知道的 10 个面试题

原文链接:10 Interview Questions Every JavaScript Developer Should Know


对大部分公司来说,招聘技术人员这种事情,管理层就应该放手交给技术团队,只有他们才能够准确地判断应聘者的技术实力。如果你恰巧是应聘者,你也是迟早都要去面试的。不管你是哪边的,都让大哥来教你几招。

大兄弟们,要收藏,也要点赞呐。

以人为本

How to Build a High Velocity Development Team 一文中,我提出了一些观点,我觉得这些观点很重要,所以在这里再重复一遍:

优秀的团队才是决定公司业绩的关键,一家公司要想于逆境之中仍能有所建树,最重要的就是得先培养出一只优秀的团队。

就像 Marcus Lemonis 说的,有三点(3 个 P)最重要:

员工(People),流程(Process),产品(Product)。

在创业初期,你招来的工程师必须是能够独当一面的大神队友。他最好能够帮着招聘工程师,能指导其它工程师,还能帮初级和中级工程师解决各种问题。这样优秀的队友,无论何时都多多益善。

要想知道面试应聘者时,有哪些常见的注意事项,可以读读 Why Hiring is So Hard in Tech 这篇文章。

要评估一个应聘者的真实水准,最佳方式就是结对编程(pair programming)。

和应聘者结对编程,一切都听应聘者的。多观察、多聆听,看看应聘者是个怎样的人。用微博的 API 抓取消息并显示在时间线上,就是个很好的考察应聘者的面试项目。

不过结对编程再好使,也没办法让你完全了解一个应聘者。这个时候,面试也能帮上很多忙——但是千万别浪费时间去问一些语法(syntax)或者语言上的细节(language quirks)——问些高端的问题吧,大兄弟。问问项目架构(architecture),编程范式(paradigms),这个层面上的判断(the big desicions)能够在很大程度上影响一个项目的成败。

语法和语言特性(features)这种小知识,Google 一搜一大把,谁都会。而工程师在工作中所积累的软件工程方面的经验,以及个人常用的编程范式及代码风格(idioms),这些可都是很难 Google 到的宝贵财富。

JavaScript 很独特,它在各种大型项目中都起着至关重要的作用。那是什么让 JavaScript 如此与众不同?

下面几个问题,也许能帮你一探究竟。


1. 能说出来两种对于 JavaScript 工程师很重要的编程范式么?

JavaScript 是一门多范式(multi-paradigm)的编程语言,它既支持命令式(imperative)/面向过程(procedural)编程,也支持面向对象编程(OOP,Object-Oriented Programming),还支持函数式编程(functional programming)。JavaScript 所支持的面向对象编程包括原型继承(prototypal inheritance)。

面试加分项

  • 原型继承(即:原型,OLOO——链接到其它对象的对象);
  • 函数式编程(即:闭包(closure),一类函数(first class functions),lambda 函数:箭头函数)。

面试减分项

  • 连范式都不知道,更别提什么原型 OO(prototypal oo)或者函数式编程了。

深入了解

2. 什么是函数式编程?

函数式编程,是将数学函数组合起来,并且避免了状态共享(shared state)及可变数据(mutable data),由此而产生的编程语言。发明于 1958 年的 Lisp 就是首批支持函数式编程的语言之一,而 λ 演算(lambda calculus)则可以说是孕育了这门语言。即使在今天,Lisp 这个家族的编程语言应用范围依然很广。

函数式编程可是 JavaScript 语言中非常重要的一个概念(它可是 JavaScript 的两大支柱之一)。ES5 规范中就增加了很多常用的函数式工具。

面试加分项

  • 纯函数(pure functions)/函数的纯粹性(function purity)
  • 知道如何避免副作用(side-effects)
  • 简单函数的组合
  • 函数式编程语言:Lisp,ML,Haskell,Erlang,Clojure,Elm,F#,OCaml,等等
  • 提到了 JavaScript 语言中支持函数式编程(FP)的特性:一类函数,高阶函数(higher order functions),作为参数(arguments)/值(values)的函数

面试减分项

  • 没有提到纯函数,以及如何避免副作用
  • 没有提供函数式编程语言的例子
  • 没有说是 JavaScript 中的哪些特性使得函数式编程得以实现

深入了解

3. 类继承和原型继承有什么区别?

类继承(Class Inheritance):实例(instances)由类继承而来(类和实例的关系,可以类比为建筑图纸和实际建筑 🏠 的关系),同时还会创建父类—子类这样一种关系,也叫做类的分层分类(hierarchical class taxonomies)。通常是用 new 关键字调用类的构造函数(constructor functions)来创建实例的。不过在 ES6 中,要继承一个类,不用 class 关键字也可以。

原型继承(Prototypal Inheritance):实例/对象直接从其它对象继承而来,创建实例的话,往往用工厂函数(factory functions)或者 Object.create() 方法。实例可以从多个不同的对象组合而来,这样就能选择性地继承了。

在 JavaScript 中,原型继承比类继承更简单,也更灵活。

面试加分项

  • 类:会创建紧密的耦合,或者说层级结构(hierarchies)/分类(taxonomies)。
  • 原型:提到了衔接继承(concatenative inheritance)、原型委托( prototype delegation)、函数继承(functional inheritance),以及对象组合(object composition)。

面试减分项

  • 原型继承和组合,与类继承相比,不知道哪个更好。

深入了解

4. 函数式编程和面向对象编程,各有什么优点和不足呢?

面向对象编程的优点:关于“对象”的一些基础概念理解起来比较容易,方法调用的含义也好解释。面向对象编程通常使用命令式的编码风格,声明式(declarative style)的用得比较少。这样的代码读起来,像是一组直接的、计算机很容易就能遵循的指令。

面向对象编程的不足:面向对象编程往往需要共享状态。对象及其行为常常会添加到同一个实体上,这样一来,如果一堆函数都要访问这个实体,而且这些函数的执行顺序不确定的话,很可能就会出乱子了,比如竞争条件(race conditions)这种现象(函数 A 依赖于实体的某个属性,但是在 A 访问属性之前,属性已经被函数 B 修改了,那么函数 A 在使用属性的时候,很可能就得不到预期的结果)。

函数式编程的优点:用函数式范式来编程,就不需要担心共享状态或者副作用了。这样就避免了几个函数在调用同一批资源时可能产生的 bug 了。拥有了“无参风格”(point-free style,也叫隐式编程)之类的特性之后,函数式编程就大大简化了,我们也可以用函数式编程的方式来把代码组合成复用性更强的代码了,面向对象编程可做不到这一点。

函数式编程更偏爱声明式、符号式(denotational style)的编码风格,这样的代码,并不是那种为了实现某种目的而需要按部就班地执行的一大堆指令,而是关注宏观上要做什么。至于具体应该怎么做,就都隐藏在函数内部了。这样一来,要是想重构代码、优化性能,那就大有可为了。(译者注:以做一道菜为例,就是由 买菜 -> 洗菜 -> 炒菜 这三步组成,每一步都是函数式编程的一个函数,不管做什么菜,这个流程都是不会变的。而想要优化这个过程,自然就是要深入每一步之中了。这样不管内部如何重构、优化,整体的流程并不会变,这就是函数式编程的好处。)甚至可以把一种算法换成另一种更高效的算法,同时还基本不需要修改代码(比如把及早求值策略(eager evaluation)替换为惰性求值策略(lazy evaluation))。

利用纯函数进行的计算,可以很方便地扩展到多处理器环境下,或者应用到分布式计算集群上,同时还不用担心线程资源冲突、竞争条件之类的问题。

函数式编程的不足:代码如果过度利用了函数式的编程特性(如无参风格、大量方法的组合),就会影响其可读性,从而简洁度有余、易读性不足。

大部分工程师还是更熟悉面向对象编程、命令式编程,对于刚接触函数式编程的人来说,即使只是这个领域的一些的简单术语,都可能让他怀疑人生。

函数式编程的学习曲线更陡峭,因为面向对象编程太普及了,学习资料太多了。相比而言,函数式编程在学术领域的应用更广泛一些,在工业界的应用稍逊一筹,自然也就不那么“平易近人”了。在探讨函数式编程时,人们往往用 λ 演算、代数、范畴学等学科的专业术语和专业符号来描述相关的概念,那么其他人想要入门函数式编程的话,就得先把这些领域的基础知识搞明白,能不让人头大么。

面试加分项

  • 共享状态的缺点、资源竞争、等等(面向对象编程)
  • 函数式编程能够极大地简化应用开发
  • 面向对象编程和函数式编程学习曲线的不同
  • 两种编程方式各自的不足之处,以及对代码后期维护带来的影响
  • 函数式风格的代码库,学习曲线会很陡峭
  • 面向对象编程风格的代码库,修改起来很难,很容易出问题(和水平相当的函数式风格的代码相比)
  • 不可变性(immutability),能够极大地提升程序历史状态(program state history)的可见性(accessible)和扩展性(malleable),这样一来,想要添加诸如无限撤销/重做、倒带/回放、可后退的调试之类的功能的话,就简单多了。不管是面向对象编程还是函数式编程,这两种范式都能实现不可变性,但是要用面向对象来实现的话,共享状态对象的数量就会剧增,代码也会变得复杂很多。

面试减分项

  • 没有讲这两种编程范式的缺点——如果熟悉至少其中一种范式的话,应该能够说出很多这种范式的缺点吧。

深入了解

总是你俩,看来你俩真是非常重要啊。

5. 什么时候该用类继承?

千万别用类继承!或者说尽量别用。如果非要用,就只用它继承一级(one level)就好了,多级的类继承简直就是反模式的。这个话题(不太明白是关于什么的……)我也参与讨论过好些年了,仅有的一些回答最终也沦为 常见的误解 之一。更多的时候,这个话题讨论着讨论着就没动静了。

如果一个特性有时候很有用
但有时候又很危险
并且还有另一种更好的特性可以用
务必要用另一种更好的特性
~ Douglas Crockford

面试加分项

  • 尽量别用,甚至是彻底不用类继承。
  • 有时候只继承一级的话也还是 OK 的,比如从框架的基类继承,例如 React.Component
  • 相比类继承,对象组合(object composition)更好一些。

深入了解

6. 什么时候该用原型继承?

原型继承可以分为下面几类:

  • 委托(delegation,也就是原型链)
  • 组合(concatenative,比如混用(mixins)、Object.assign()
  • 函数式(functional,这个函数式原型继承不是函数式编程。这里的函数是用来创建一个闭包,以实现私有状态(private state)或者封装(encapsulation))

上面这三种原型继承都有各自的适用场景,不过它们都很有用,因为都能实现组合继承(composition),也就是建立了 A 拥有特性 B(has-a)、A 用到了特性 B(uses-a) 或者 A 可以实现特性 B(can-do) 的这样一种关系。相比而言,类继承建立的是 A 就是 B 这样一种关系。

面试加分项

  • 知道在什么情况下不适合用模块化(modules)或者函数式编程。
  • 知道需要组合多个不同来源的对象时,应该怎么做。
  • 知道什么时候该用继承。

面试减分项

  • 不知道什么时候应该用原型。
  • 不知道混用和 Object.assign()

深入了解

7. 为什么说“对象组合比类继承更好”?

这句话引用的是《设计花纹》(Design Patterns,设计模式)这本书的内容。意思是要想实现代码重用,就应该把一堆小的功能单元组合成满足需求的各种对象,而不是通过类继承弄出来一层一层的对象。

换句话说,就是尽量编程实现 can-dohas-a 或者 uses-a 这种关系,而不是 is-a 这种关系。

面试加分项

  • 避免使用类继承。
  • 避免使用问题多多的基类。
  • 避免紧耦合。
  • 避免极其不灵活的层次分类(taxonomy)(类继承所产生的 is-a 关系可能会导致很多误用的情况)
  • 避免大猩猩香蕉问题(“你只是想要一根香蕉,结果最后却整出来一只拿着香蕉的大猩猩,还有整个丛林”)。
  • 要让代码更具扩展性。

面试减分项

  • 没有提到上面任何一种问题。
  • 没有表达清楚对象组合与类继承有什么区别,也没有提到对象组合的优点。

深入了解

8. 双向数据绑定/单向数据流的含义和区别

双向数据绑定(two-way data binding),意味着 UI 层所呈现的内容和 Model 层的数据动态地绑定在一起了,其中一个发生了变化,就会立刻反映在另一个上。比如用户在前端页面的表单控件中输入了一个值,Model 层对应该控件的变量就会立刻更新为用户所输入的值;反之亦然,如果 Modal 层的数据有变化,变化后的数据也会立刻反映至 UI 层。

单向数据流(one-way data flow), 意味着只有 Model 层才是单一数据源(single source of truth)。UI 层的变化会触发对应的消息机制,告知 Model 层用户的目的(对应 React 的 store)。只有 Model 层才有更改应用状态的权限,这样一来,数据永远都是单向流动的,也就更容易了解应用的状态是如何变化的。

采用单向数据流的应用,其状态的变化是很容易跟踪的,采用双向数据绑定的应用,就很难跟踪并理解状态的变化了。

面试加分项

  • React 是单向数据流的典型,面试时提到这个框架的话会加分。Cycle.js 则是另一个很流行的单向数据流的库。
  • Angular 则是双向数据绑定的典型。

面试减分项

  • 不理解单向数据流/双向数据绑定的含义,也说不清楚两者之间的区别。

深入了解

9. 单体架构和微服务架构各有何优劣?

采用单体架构(monolithic architecture)的应用,各组件的代码是作为一个整体存在的,组件之间互相合作,共享内存和资源。

而微服务架构(microservice architecture)则是由许许多多个互相独立的小应用组成,每个应用都有自己的内存空间,应用在扩容时也是独立于其它应用进行的。

单体架构的优势:大部分应用都有相当数量的横切关注点(cross-cutting concerns),比如日志记录,流量限制,还有审计跟踪和 DOS 防护等安全方面的需求,单体架构在这方面就很有优势。

当所有功能都运行在一个应用里的时候,就可以很方便地将组件与横切关注点相关联。

单体架构也有性能上的优势,毕竟访问共享内存还是比进程间通信(inter-process communication,IPC)要快的。

单体架构的劣势:随着单体架构应用功能的不断开发,各项服务之间的耦合程度也会不断增加,这样一来就很难把各项服务分离开来了,要做独立扩容或者代码维护也就更不方便了。

微服务的优势:微服务架构一般都有更好的组织结构,因为每项服务都有自己特定的分工,而且也不会干涉其它组件所负责的部分。服务解耦之后,想要重新组合、配置来为各个不同的应用提供服务的话,也更方便了(比如同时为 Web 客户端和公共 API 提供服务)。

如果用合理的架构来部署微服务的话,它在性能上也是很有优势的,因为这样一来,就可以很轻松地分离热门服务,对其进行扩容,同时还不会影响到应用中的其它部分。

微服务的劣势:在实际构建一个新的微服务架构的时候,会遇到很多在设计阶段没有预料到的横切关注点。如果是单体架构应用的话就很简单,新建一个中间件(shared magic helpers 不知道怎么翻译……)来解决这样的问题就行了,没什么麻烦的。

但是在微服务架构中就不一样了,要解决这个问题,要么为每个横切关注点都引入一个独立的模块,要么就把所有横切关注点的解决方案封装到一个服务层中,让所有流量都从这里走一遍就行了。

为了解决横切关注点的问题,虽然单体架构也趋向于把所有的路由流量都从一个外部服务层走一遍,但是在这种架构中,可以等到项目非常成熟之后再进行这种改造,这样就可以把还这笔技术债的时间尽量往后拖一拖。

微服务一般都是部署在虚拟机或容器上的,随着应用规模的不断增加,虚拟机抢工作(VM wrangling work)的情况也会迅速增加。任务的分配一般都是通过容器群(container fleet)管理工具来自动实现的。

面试加分项

  • 对于微服务的积极态度,虽然初始成本会比单体架构要高一些。知道微服务的性能和扩容在长期看来表现更佳。
  • 在微服务架构和单体架构应用上都有实战经验。能够使应用中的各项服务在代码层面互相独立,但是又可以在开发初期迅速地将各项服务打包成一整个的单体架构应用。微服务化的改造可以在应用相当成熟之后,改造成本在可承受范围内的时候再进行。

面试减分项

  • 不知道单体架构和微服务架构的区别。
  • 不知道微服务架构额外的开销,或者没有实际经验。
  • 不知道微服务架构中,IPC 和网络通信所导致的额外的性能开销。
  • 过分贬低微服务。说不清楚什么时候应该把单体架构应用解耦成微服务。
  • 低估了可独立扩容的微服务的优势。

10. 异步编程是什么?又为什么在 JavaScript 中这么重要?

在同步编程中,代码会按顺序自顶向下依次执行(条件语句和函数调用除外),如果遇到网络请求或者磁盘读/写(I/O)这类耗时的任务,就会堵塞在这样的地方。

在异步编程中,JS 运行在事件循环(event loop)中。当需要执行一个阻塞操作(blocking operation)时,主线程发起一个(异步)请求,(工作线程就会去执行这个异步操作,)同时主线程继续执行后面的代码。(工作线程执行完毕之后,)就会发起响应,触发中断(interrupt),执行事件处理程序(event handler),执行完后主线程继续往后走。这样一来,一个程序线程就可以处理大量的并发操作了。

用户界面(user interface,UI)天然就是异步的,大部分时间它都在等待用户输入,从而中断事件循环,触发事件处理程序。

Node.js 默认是异步的,采用它构建的服务端和用户界面的执行机制差不多,在事件循环中等待网络请求,然后一个接一个地处理这些请求。

异步在 JavaScript 中非常重要,因为它既适合编写 UI,在服务端也有上佳的性能表现。

面试加分项

  • 理解阻塞的含义,以及对性能带来的影响。
  • 理解事件处理程序,以及它为什么对 UI 部分的代码很重要。

面试减分项

  • 不熟悉同步、异步的概念。
  • 讲不清楚异步代码和 UI 代码的性能影响,也说不明白它俩之间的关系。

总结

多问问应聘者高层次的知识点,如果能讲清楚这些概念,就说明即使应聘者没怎么接触过 JavaScript,也能够在短短几个星期之内就把语言细节和语法之类的东西弄清楚。

不要因为应聘者在一些简单的知识上表现不佳就把对方 pass 掉,比如经典的 CS-101 算法课,或者一些解谜类的题目。

面试官真正应该关注的,是应聘者是否知道如何把一堆功能组织在一起,形成一个完整的应用。

电话面试的注意点就这些了,在线下的面试中,我更加关注应聘者实际编写代码的能力,我会观察他如何写代码。在我的《精通 JavaScript 面试》这个系列文章中,会有更深入的描述。

查看原文

认证与成就

  • 获得 30 次点赞
  • 获得 37 枚徽章 获得 1 枚金徽章, 获得 8 枚银徽章, 获得 28 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2015-08-20
个人主页被 1.1k 人浏览